From 30634179cdbc4d2c41518f83f52a4a26097c9011 Mon Sep 17 00:00:00 2001 From: Martin Biel Date: Wed, 6 May 2020 16:00:22 +0200 Subject: [PATCH 1/5] Add support for custom objective functions in UniversalFallback. --- src/Utilities/universalfallback.jl | 78 +++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/src/Utilities/universalfallback.jl b/src/Utilities/universalfallback.jl index 39cf7c45e6..3286e3ad9a 100644 --- a/src/Utilities/universalfallback.jl +++ b/src/Utilities/universalfallback.jl @@ -2,17 +2,18 @@ UniversalFallback The `UniversalFallback` can be applied on a [`MathOptInterface.ModelLike`](@ref) -`model` to create the model `UniversalFallback(model)` supporting *any* -constraint and attribute. This allows to have a specialized implementation in -`model` for performance critical constraints and attributes while still +`model` to create the model `UniversalFallback(model)` supporting *any* objective, +constraint, and attribute. This allows to have a specialized implementation in +`model` for performance critical objective/constraints and attributes while still supporting other attributes with a small performance penalty. Note that `model` -is unaware of constraints and attributes stored by `UniversalFallback` so this +is unaware of objective/constraints and attributes stored by `UniversalFallback` so this is not appropriate if `model` is an optimizer (for this reason, [`MathOptInterface.optimize!`](@ref) has not been implemented). In that case, optimizer bridges should be used instead. """ mutable struct UniversalFallback{MT} <: MOI.ModelLike model::MT + objective::Union{MOI.AbstractScalarFunction, Nothing} constraints::OrderedDict{Tuple{DataType, DataType}, OrderedDict} # See https://github.com/JuliaOpt/JuMP.jl/issues/1152 and https://github.com/JuliaOpt/JuMP.jl/issues/2238 nextconstraintid::Int64 con_to_name::Dict{CI, String} @@ -23,6 +24,7 @@ mutable struct UniversalFallback{MT} <: MOI.ModelLike conattr::Dict{MOI.AbstractConstraintAttribute, Dict{CI, Any}} function UniversalFallback{MT}(model::MOI.ModelLike) where {MT} new{typeof(model)}(model, + nothing, OrderedDict{Tuple{DataType, DataType}, OrderedDict}(), 0, Dict{CI, String}(), @@ -39,10 +41,11 @@ function Base.show(io::IO, U::UniversalFallback) s(n) = n == 1 ? "" : "s" indent = " "^get(io, :indent, 0) MOIU.print_with_acronym(io, summary(U)) + !isnothing(U.objective) && print(io, "\n$(indent)with objective") for (attr, name) in ( (U.constraints, "constraint"), - (U.optattr, "optimizer attribute"), - (U.modattr, "model attribute"), - (U.varattr, "variable attribute"), + (U.optattr, "optimizer attribute"), + (U.modattr, "model attribute"), + (U.varattr, "variable attribute"), (U.conattr, "constraint attribute") ) n = length(attr) if n > 0 @@ -54,11 +57,12 @@ function Base.show(io::IO, U::UniversalFallback) end function MOI.is_empty(uf::UniversalFallback) - return MOI.is_empty(uf.model) && isempty(uf.constraints) && + return MOI.is_empty(uf.model) && isnothing(uf.objective) && isempty(uf.constraints) && isempty(uf.modattr) && isempty(uf.varattr) && isempty(uf.conattr) end function MOI.empty!(uf::UniversalFallback) MOI.empty!(uf.model) + uf.objective = nothing empty!(uf.constraints) uf.nextconstraintid = 0 empty!(uf.con_to_name) @@ -156,6 +160,9 @@ function MOI.delete(uf::UniversalFallback, vi::VI) for d in values(uf.varattr) delete!(d, vi) end + if !isnothing(uf.objective) + uf.objective = remove_variable(uf.objective, vi) + end for (_, constraints) in uf.constraints _remove_variable(uf, constraints, vi) end @@ -167,6 +174,9 @@ function MOI.delete(uf::UniversalFallback, vis::Vector{VI}) delete!(d, vi) end end + if !isnothing(uf.objective) + uf.objective = remove_variable(uf.objective, vis) + end for (_, constraints) in uf.constraints _remove_vector_of_variables(uf, constraints, vis) for vi in vis @@ -248,6 +258,9 @@ function MOI.get(uf::UniversalFallback, listattr::MOI.ListOfOptimizerAttributesS end function MOI.get(uf::UniversalFallback, listattr::MOI.ListOfModelAttributesSet) list = MOI.get(uf.model, listattr) + if !isnothing(uf.objective) + push!(list, MOI.ObjectiveFunction{typeof(uf.objective)}()) + end for attr in keys(uf.modattr) push!(list, attr) end @@ -268,6 +281,52 @@ function MOI.get(uf::UniversalFallback, listattr::MOI.ListOfConstraintAttributes return list end +# Objective +MOI.supports(uf::UniversalFallback, ::MOI.ObjectiveSense) = true +function MOI.set(uf::UniversalFallback, attr::MOI.ObjectiveSense, + sense::MOI.OptimizationSense) where T + if sense == MOI.FEASIBILITY_SENSE + uf.objective = nothing + end + MOI.set(uf.model, attr, sense) +end +function MOI.get(uf::UniversalFallback, + attr::MOI.ObjectiveFunctionType) + if isnothing(uf.objective) + return MOI.get(uf.model, attr) + else + return typeof(uf.objective) + end +end +function MOI.get(uf::UniversalFallback, + attr::MOI.ObjectiveFunction{F})::F where F + if isnothing(uf.objective) + return MOI.get(uf.model, attr) + else + return uf.objective + end +end +MOI.supports(uf::UniversalFallback, ::MOI.ObjectiveFunction{F}) where F <: MOI.AbstractScalarFunction = true +function MOI.set(uf::UniversalFallback, + attr::MOI.ObjectiveFunction, + func::MOI.AbstractScalarFunction) + if MOI.supports(uf.model, attr) + MOI.set(uf.model, attr, func) + uf.objective = nothing + else + uf.objective = copy(func) + uf.model.objectiveset = false + end +end + +function MOI.modify(uf::UniversalFallback, obj::MOI.ObjectiveFunction, change::MOI.AbstractFunctionModification) where F + if isnothing(uf.objective) + MOI.modify(uf.model, obj, change) + else + uf.objective = modify_function(uf.objective, change) + end +end + # Name # The names of constraints not supported by `uf.model` need to be handled function MOI.set(uf::UniversalFallback, attr::MOI.ConstraintName, ci::CI{F, S}, name::String) where {F, S} @@ -430,9 +489,6 @@ function MOI.set(uf::UniversalFallback, ::MOI.ConstraintSet, ci::CI{F,S}, set::S end end -# Objective -MOI.modify(uf::UniversalFallback, obj::MOI.ObjectiveFunction, change::MOI.AbstractFunctionModification) = MOI.modify(uf.model, obj, change) - # Variables MOI.add_variable(uf::UniversalFallback) = MOI.add_variable(uf.model) MOI.add_variables(uf::UniversalFallback, n) = MOI.add_variables(uf.model, n) From 503bda034e49f8f0bc822c65d17f0bd0a60d6773 Mon Sep 17 00:00:00 2001 From: Martin Biel Date: Wed, 6 May 2020 19:00:49 +0200 Subject: [PATCH 2/5] Changed isnothing(x) to x === nothing for 1.0 support. --- src/Utilities/universalfallback.jl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Utilities/universalfallback.jl b/src/Utilities/universalfallback.jl index 3286e3ad9a..24c513bf11 100644 --- a/src/Utilities/universalfallback.jl +++ b/src/Utilities/universalfallback.jl @@ -41,7 +41,7 @@ function Base.show(io::IO, U::UniversalFallback) s(n) = n == 1 ? "" : "s" indent = " "^get(io, :indent, 0) MOIU.print_with_acronym(io, summary(U)) - !isnothing(U.objective) && print(io, "\n$(indent)with objective") + !(U.objective === nothing) && print(io, "\n$(indent)with objective") for (attr, name) in ( (U.constraints, "constraint"), (U.optattr, "optimizer attribute"), (U.modattr, "model attribute"), @@ -57,7 +57,7 @@ function Base.show(io::IO, U::UniversalFallback) end function MOI.is_empty(uf::UniversalFallback) - return MOI.is_empty(uf.model) && isnothing(uf.objective) && isempty(uf.constraints) && + return MOI.is_empty(uf.model) && uf.objective === nothing && isempty(uf.constraints) && isempty(uf.modattr) && isempty(uf.varattr) && isempty(uf.conattr) end function MOI.empty!(uf::UniversalFallback) @@ -160,7 +160,7 @@ function MOI.delete(uf::UniversalFallback, vi::VI) for d in values(uf.varattr) delete!(d, vi) end - if !isnothing(uf.objective) + if !(uf.objective === nothing) uf.objective = remove_variable(uf.objective, vi) end for (_, constraints) in uf.constraints @@ -174,7 +174,7 @@ function MOI.delete(uf::UniversalFallback, vis::Vector{VI}) delete!(d, vi) end end - if !isnothing(uf.objective) + if !(uf.objective === nothing) uf.objective = remove_variable(uf.objective, vis) end for (_, constraints) in uf.constraints @@ -258,7 +258,7 @@ function MOI.get(uf::UniversalFallback, listattr::MOI.ListOfOptimizerAttributesS end function MOI.get(uf::UniversalFallback, listattr::MOI.ListOfModelAttributesSet) list = MOI.get(uf.model, listattr) - if !isnothing(uf.objective) + if !uf.objective === nothing push!(list, MOI.ObjectiveFunction{typeof(uf.objective)}()) end for attr in keys(uf.modattr) @@ -292,7 +292,7 @@ function MOI.set(uf::UniversalFallback, attr::MOI.ObjectiveSense, end function MOI.get(uf::UniversalFallback, attr::MOI.ObjectiveFunctionType) - if isnothing(uf.objective) + if uf.objective === nothing return MOI.get(uf.model, attr) else return typeof(uf.objective) @@ -300,7 +300,7 @@ function MOI.get(uf::UniversalFallback, end function MOI.get(uf::UniversalFallback, attr::MOI.ObjectiveFunction{F})::F where F - if isnothing(uf.objective) + if uf.objective === nothing return MOI.get(uf.model, attr) else return uf.objective @@ -320,7 +320,7 @@ function MOI.set(uf::UniversalFallback, end function MOI.modify(uf::UniversalFallback, obj::MOI.ObjectiveFunction, change::MOI.AbstractFunctionModification) where F - if isnothing(uf.objective) + if uf.objective === nothing MOI.modify(uf.model, obj, change) else uf.objective = modify_function(uf.objective, change) From 83f5f83265d8011ca42a826c8e21cfb1bdb3a081 Mon Sep 17 00:00:00 2001 From: Martin Biel Date: Wed, 6 May 2020 19:08:30 +0200 Subject: [PATCH 3/5] Fixed missing parantheses --- src/Utilities/universalfallback.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Utilities/universalfallback.jl b/src/Utilities/universalfallback.jl index 24c513bf11..2ee1374d29 100644 --- a/src/Utilities/universalfallback.jl +++ b/src/Utilities/universalfallback.jl @@ -258,7 +258,7 @@ function MOI.get(uf::UniversalFallback, listattr::MOI.ListOfOptimizerAttributesS end function MOI.get(uf::UniversalFallback, listattr::MOI.ListOfModelAttributesSet) list = MOI.get(uf.model, listattr) - if !uf.objective === nothing + if !(uf.objective === nothing) push!(list, MOI.ObjectiveFunction{typeof(uf.objective)}()) end for attr in keys(uf.modattr) From 5d5adcc4df453e99abc0a8c6a71fddf674501d66 Mon Sep 17 00:00:00 2001 From: Martin Biel Date: Thu, 7 May 2020 09:49:55 +0200 Subject: [PATCH 4/5] Revised according to review comments by blegat --- src/Utilities/universalfallback.jl | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Utilities/universalfallback.jl b/src/Utilities/universalfallback.jl index 2ee1374d29..cdad5cf3de 100644 --- a/src/Utilities/universalfallback.jl +++ b/src/Utilities/universalfallback.jl @@ -2,11 +2,11 @@ UniversalFallback The `UniversalFallback` can be applied on a [`MathOptInterface.ModelLike`](@ref) -`model` to create the model `UniversalFallback(model)` supporting *any* objective, -constraint, and attribute. This allows to have a specialized implementation in -`model` for performance critical objective/constraints and attributes while still +`model` to create the model `UniversalFallback(model)` supporting *any* +constraint and attribute. This allows to have a specialized implementation in +`model` for performance critical constraints and attributes while still supporting other attributes with a small performance penalty. Note that `model` -is unaware of objective/constraints and attributes stored by `UniversalFallback` so this +is unaware of constraints and attributes stored by `UniversalFallback` so this is not appropriate if `model` is an optimizer (for this reason, [`MathOptInterface.optimize!`](@ref) has not been implemented). In that case, optimizer bridges should be used instead. @@ -160,7 +160,7 @@ function MOI.delete(uf::UniversalFallback, vi::VI) for d in values(uf.varattr) delete!(d, vi) end - if !(uf.objective === nothing) + if uf.objective !== nothing uf.objective = remove_variable(uf.objective, vi) end for (_, constraints) in uf.constraints @@ -174,7 +174,7 @@ function MOI.delete(uf::UniversalFallback, vis::Vector{VI}) delete!(d, vi) end end - if !(uf.objective === nothing) + if uf.objective !== nothing uf.objective = remove_variable(uf.objective, vis) end for (_, constraints) in uf.constraints @@ -258,7 +258,7 @@ function MOI.get(uf::UniversalFallback, listattr::MOI.ListOfOptimizerAttributesS end function MOI.get(uf::UniversalFallback, listattr::MOI.ListOfModelAttributesSet) list = MOI.get(uf.model, listattr) - if !(uf.objective === nothing) + if uf.objective !== nothing push!(list, MOI.ObjectiveFunction{typeof(uf.objective)}()) end for attr in keys(uf.modattr) @@ -282,7 +282,6 @@ function MOI.get(uf::UniversalFallback, listattr::MOI.ListOfConstraintAttributes end # Objective -MOI.supports(uf::UniversalFallback, ::MOI.ObjectiveSense) = true function MOI.set(uf::UniversalFallback, attr::MOI.ObjectiveSense, sense::MOI.OptimizationSense) where T if sense == MOI.FEASIBILITY_SENSE @@ -306,16 +305,19 @@ function MOI.get(uf::UniversalFallback, return uf.objective end end -MOI.supports(uf::UniversalFallback, ::MOI.ObjectiveFunction{F}) where F <: MOI.AbstractScalarFunction = true function MOI.set(uf::UniversalFallback, attr::MOI.ObjectiveFunction, func::MOI.AbstractScalarFunction) if MOI.supports(uf.model, attr) MOI.set(uf.model, attr, func) + # Clear any fallback objective uf.objective = nothing else uf.objective = copy(func) - uf.model.objectiveset = false + # Clear any `model` objective + sense = MOI.get(uf.model, MOI.ObjectiveSense()) + MOI.set(uf.model, MOI.ObjectiveSense(), MOI.FEASIBILITY_SENSE) + MOI.set(uf.model, MOI.ObjectiveSense(), sense) end end From 782aab2da56dd42d4aca3c7ce32ddcb967399343 Mon Sep 17 00:00:00 2001 From: Martin Biel Date: Fri, 29 May 2020 14:49:07 +0200 Subject: [PATCH 5/5] Added tests for custom objective in universal fallback. --- test/Utilities/universalfallback.jl | 40 +++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/test/Utilities/universalfallback.jl b/test/Utilities/universalfallback.jl index 38fc8df1df..1aba511f70 100644 --- a/test/Utilities/universalfallback.jl +++ b/test/Utilities/universalfallback.jl @@ -52,7 +52,7 @@ end struct UnknownOptimizerAttribute <: MOI.AbstractOptimizerAttribute end -# A few constraint types are supported to test both the fallback and the +# A few objective/constraint types are supported to test both the fallback and the # delegation to the internal model @MOIU.model(ModelForUniversalFallback, (), @@ -63,6 +63,11 @@ struct UnknownOptimizerAttribute <: MOI.AbstractOptimizerAttribute end (MOI.ScalarAffineFunction,), (), ()) +function MOI.supports( + ::ModelForUniversalFallback{T}, + ::Type{MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}}) where T + return false +end function MOI.supports_constraint( ::ModelForUniversalFallback{T}, ::Type{MOI.SingleVariable}, ::Type{<:Union{MOI.EqualTo{T}, MOI.GreaterThan{T}, MOI.Interval{T}, @@ -123,6 +128,37 @@ y, z = MOI.add_variables(uf, 2) listattr = MOI.ListOfVariableAttributesSet() test_varconattrs(uf, model, attr, listattr, VI, MOI.add_variable, x, y, z) end +@testset "Objective Attribute" begin + global x, y, z + _single(vi) = MOI.SingleVariable(vi) + _affine(vi) = convert(MOI.ScalarAffineFunction{Float64}, MOI.SingleVariable(vi)) + function _add_single_objective(vi) + return MOI.set(uf, MOI.ObjectiveFunction{MOI.SingleVariable}(), _single(vi)) + end + function _add_affine_objective(vi) + return MOI.set(uf, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), _affine(vi)) + end + @testset "Supported objective" begin + F = MOI.SingleVariable + @test MOI.supports(model, MOI.ObjectiveFunction{F}()) + _add_single_objective(x) + @test MOI.get(uf, MOI.ObjectiveFunction{F}()) ≈ _single(x) + MOI.set(uf, MOI.ObjectiveFunction{F}(), _single(y)) + @test MOI.get(uf, MOI.ObjectiveFunction{F}()) ≈ _single(y) + end + @testset "Unsupported objective" begin + F = MOI.ScalarAffineFunction{Float64} + @test !MOI.supports(model, MOI.ObjectiveFunction{F}) + @test MOI.supports(uf, MOI.ObjectiveFunction{F}()) + _add_affine_objective(x) + @test MOI.get(uf, MOI.ObjectiveFunction{F}()) ≈ _affine(x) + MOI.modify(uf, MOI.ObjectiveFunction{F}(), MOI.ScalarCoefficientChange(y, 1.)) + fx = MOI.SingleVariable(x) + fy = MOI.SingleVariable(y) + obj = 1fx + 1fy + @test MOI.get(uf, MOI.ObjectiveFunction{F}()) ≈ obj + end +end @testset "Constraint Attribute" begin global x, y, z _affine(vi) = convert(MOI.ScalarAffineFunction{Float64}, MOI.SingleVariable(vi)) @@ -221,7 +257,7 @@ end cy2 = MOI.add_constraint(uf, _affine(y), sets[2]) # check that the constraint types are in the order they were added in @test MOI.get(uf, MOI.ListOfConstraints()) == [(F, typeof(sets[1])), (F, typeof(sets[2]))] - # check that the constraints given the constraint type are in the order they were added in + # check that the constraints given the constraint type are in the order they were added in @test MOI.get(uf, MOI.ListOfConstraintIndices{F, typeof(sets[1])}()) == [MOI.ConstraintIndex{F, typeof(sets[1])}(1), MOI.ConstraintIndex{F, typeof(sets[1])}(3)] @test MOI.get(uf, MOI.ListOfConstraintIndices{F, typeof(sets[2])}()) == [MOI.ConstraintIndex{F, typeof(sets[2])}(2), MOI.ConstraintIndex{F, typeof(sets[2])}(4)] end