diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 7a717d5..def730e 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -567,8 +567,11 @@ function MOI.supports( # We can't tell at type-time whether the constraints will be quadratic or # lowered to affine, so we return the conservative choice for supports of # needing to support names for both quadratic and affine constraints. - return MOI.supports(model.optimizer, attr, MOI.ConstraintIndex{F,S}) && - MOI.supports(model.optimizer, attr, MOI.ConstraintIndex{G,S}) + if MOI.supports_constraint(model.optimizer, F, S) + return MOI.supports(model.optimizer, attr, MOI.ConstraintIndex{F,S}) && + MOI.supports(model.optimizer, attr, MOI.ConstraintIndex{G,S}) + end + return MOI.supports(model.optimizer, attr, MOI.ConstraintIndex{G,S}) end function MOI.supports( @@ -580,8 +583,14 @@ function MOI.supports( # We can't tell at type-time whether the constraints will be quadratic or # lowered to affine, so we return the conservative choice for supports of # needing to support names for both quadratic and affine constraints. - return MOI.supports(model.optimizer, attr, MOI.ConstraintIndex{F,S}) && - MOI.supports(model.optimizer, attr, MOI.ConstraintIndex{G,S}) + # TODO: + # switch to only check support name for the case of linear + # is a solver does not support quadratic constraints it will fain in add_ + if MOI.supports_constraint(model.optimizer, F, S) + return MOI.supports(model.optimizer, attr, MOI.ConstraintIndex{F,S}) && + MOI.supports(model.optimizer, attr, MOI.ConstraintIndex{G,S}) + end + return MOI.supports(model.optimizer, attr, MOI.ConstraintIndex{G,S}) end function MOI.set( @@ -804,10 +813,11 @@ end function MOI.modify( model::Optimizer, c::MOI.ConstraintIndex{F,S}, - chg::MOI.ScalarCoefficientChange{T}, + chg::Union{MOI.ScalarConstantChange{T},MOI.ScalarCoefficientChange{T}}, ) where {F,S,T} - if haskey(model.quadratic_constraint_cache, c) || - haskey(model.affine_constraint_cache, c) + if haskey(model.quadratic_outer_to_inner, c) || + haskey(model.vector_quadratic_outer_to_inner, c) || + haskey(model.affine_outer_to_inner, c) error( "Parametric constraint cannot be modified in ParametricOptInterface, because it would conflict with parameter updates. You can update the parameters instead.", ) @@ -1365,16 +1375,47 @@ end function MOI.get( model::Optimizer, ::MOI.ListOfConstraintAttributesSet{F,S}, -) where {F,S} - if F <: MOI.ScalarQuadraticFunction - error( - "MOI.ListOfConstraintAttributesSet is not implemented for ScalarQuadraticFunction in ParametricOptInterface.", - ) - elseif F <: MOI.VectorQuadraticFunction - error( - "MOI.ListOfConstraintAttributesSet is not implemented for VectorQuadraticFunction in ParametricOptInterface.", - ) +) where {T,F<:MOI.ScalarQuadraticFunction{T},S} + if MOI.supports_constraint(model.optimizer, F, S) + # in this case we cant tell if the constraint will be quadratic or + # lowered to affine + if model.warn_quad_affine_ambiguous + println( + "MOI.ListOfConstraintAttributesSet is not supported for ScalarQuadraticFunction in ParametricOptInterface, an empty list will be returned. This message can be suppressed by setting `POI._WarnIfQuadraticOfAffineFunctionAmbiguous` to false.", + ) + end + return [] + end + return MOI.get( + model.optimizer, + MOI.ListOfConstraintAttributesSet{MOI.ScalarAffineFunction{T},S}(), + ) +end + +function MOI.get( + model::Optimizer, + ::MOI.ListOfConstraintAttributesSet{F,S}, +) where {T,F<:MOI.VectorQuadraticFunction{T},S} + if MOI.supports_constraint(model.optimizer, F, S) + # in this case we cant tell if the constraint will be quadratic or + # lowered to affine + if model.warn_quad_affine_ambiguous + println( + "MOI.ListOfConstraintAttributesSet is not supported for VectorQuadraticFunction in ParametricOptInterface, an empty list will be returned. This message can be suppressed by setting `POI._WarnIfQuadraticOfAffineFunctionAmbiguous` to false.", + ) + end + return [] end + return MOI.get( + model.optimizer, + MOI.ListOfConstraintAttributesSet{MOI.VectorAffineFunction{T},S}(), + ) +end + +function MOI.get( + model::Optimizer, + ::MOI.ListOfConstraintAttributesSet{F,S}, +) where {F,S} return MOI.get(model.optimizer, MOI.ListOfConstraintAttributesSet{F,S}()) end @@ -1405,7 +1446,7 @@ function MOI.get( model::Optimizer, ::MOI.ListOfConstraintIndices{F,S}, ) where {S,F} - list = collect(values(model.constraint_outer_to_inner[F, S])) + list = collect(keys(model.constraint_outer_to_inner[F, S])) sort!(list, lt = (x, y) -> (x.value < y.value)) return list end @@ -2115,3 +2156,27 @@ end function MOI.Utilities.final_touch(model::Optimizer, index_map) return MOI.Utilities.final_touch(model.optimizer, index_map) end + +""" + _WarnIfQuadraticOfAffineFunctionAmbiguous + +Some attributes such as `MOI.ListOfConstraintAttributesSet` are ambiguous +when the model contains parametric quadratic functions that can be lowered +to affine functions. This attribute can be set to `false` to skip the warning +when such ambiguity arises. The default value is `true`. +""" +struct _WarnIfQuadraticOfAffineFunctionAmbiguous <: + MOI.AbstractOptimizerAttribute end + +function MOI.set( + model::Optimizer, + ::_WarnIfQuadraticOfAffineFunctionAmbiguous, + value::Bool, +) + model.warn_quad_affine_ambiguous = value + return +end + +function MOI.get(model::Optimizer, ::_WarnIfQuadraticOfAffineFunctionAmbiguous) + return model.warn_quad_affine_ambiguous +end diff --git a/src/ParametricOptInterface.jl b/src/ParametricOptInterface.jl index 4c08b86..5c2080a 100644 --- a/src/ParametricOptInterface.jl +++ b/src/ParametricOptInterface.jl @@ -179,6 +179,8 @@ mutable struct Optimizer{T,OT<:MOI.ModelLike} <: MOI.AbstractOptimizer parameters_in_conflict::Set{MOI.VariableIndex} + warn_quad_affine_ambiguous::Bool + # extension data ext::Dict{Symbol,Any} function Optimizer{T}( @@ -243,6 +245,7 @@ mutable struct Optimizer{T,OT<:MOI.ModelLike} <: MOI.AbstractOptimizer ONLY_CONSTRAINTS, save_original_objective_and_constraints, Set{MOI.VariableIndex}(), + true, Dict{Symbol,Any}(), ) end diff --git a/test/jump_tests.jl b/test/jump_tests.jl index aa8a639..89353a2 100644 --- a/test/jump_tests.jl +++ b/test/jump_tests.jl @@ -1654,24 +1654,163 @@ function test_jump_errors() SCS.Optimizer(), ) optimizer1 = POI.Optimizer(cached1) - model1 = direct_model(optimizer1) + model = direct_model(optimizer1) @test_throws MOI.UnsupportedAttribute MOI.get( - backend(model1), + backend(model), MOI.NLPBlock(), ) - @test_throws ErrorException MOI.get( - backend(model1), + + MOI.get( + backend(model), MOI.ListOfConstraintAttributesSet{ MOI.VectorQuadraticFunction{Float64}, - MOI.Nonpositives, + MOI.Nonnegatives, }(), ) - @test_throws ErrorException MOI.get( - backend(model1), + + MOI.get( + backend(model), MOI.ListOfConstraintAttributesSet{ MOI.ScalarQuadraticFunction{Float64}, - MOI.EqualTo{Float64}, + MOI.LessThan{Float64}, + }(), + ) + + MOI.set( + backend(model), + POI._WarnIfQuadraticOfAffineFunctionAmbiguous(), + false, + ) + + @test MOI.get( + backend(model), + POI._WarnIfQuadraticOfAffineFunctionAmbiguous(), + ) == false + + MOI.get( + backend(model), + MOI.ListOfConstraintAttributesSet{ + MOI.VectorQuadraticFunction{Float64}, + MOI.Nonnegatives, + }(), + ) + + MOI.get( + backend(model), + MOI.ListOfConstraintAttributesSet{ + MOI.ScalarQuadraticFunction{Float64}, + MOI.LessThan{Float64}, + }(), + ) + + model = direct_model(POI.Optimizer(Ipopt.Optimizer())) + + @test_throws MOI.GetAttributeNotAllowed MOI.get( + backend(model), + MOI.ListOfConstraintAttributesSet{ + MOI.VectorQuadraticFunction{Float64}, + MOI.Nonnegatives, + }(), + ) + + MOI.get( + backend(model), + MOI.ListOfConstraintAttributesSet{ + MOI.ScalarQuadraticFunction{Float64}, + MOI.LessThan{Float64}, + }(), + ) + + MOI.set( + backend(model), + POI._WarnIfQuadraticOfAffineFunctionAmbiguous(), + false, + ) + + @test MOI.get( + backend(model), + POI._WarnIfQuadraticOfAffineFunctionAmbiguous(), + ) == false + + @test_throws MOI.GetAttributeNotAllowed MOI.get( + backend(model), + MOI.ListOfConstraintAttributesSet{ + MOI.VectorQuadraticFunction{Float64}, + MOI.Nonnegatives, + }(), + ) + + MOI.get( + backend(model), + MOI.ListOfConstraintAttributesSet{ + MOI.ScalarQuadraticFunction{Float64}, + MOI.LessThan{Float64}, + }(), + ) + + return +end + +function test_print() + model = direct_model(POI.Optimizer(HiGHS.Optimizer())) + @variable(model, p in MOI.Parameter(1.0)) + @variable(model, x) + @constraint(model, con, x >= p + p * p + p * x) + @objective(model, Min, 1 + 2x) + filename = tempdir() * "/test.lp" + write_to_file(model, filename) + @test readlines(filename) == [ + "minimize", + "obj: 1 + 2 x", + "subject to", + "con: 1 x - 1 p + [ -1 p * x - 1 p ^ 2 ] >= 0", + "Bounds", + "x free", + "p = 1", + "End", + ] + return +end + +function test_set_normalized_coefficient() + model = direct_model(POI.Optimizer(HiGHS.Optimizer())) + @variable(model, p in MOI.Parameter(1.0)) + @variable(model, x) + @constraint(model, con, x >= p) + @constraint(model, con1, x >= 1) + @constraint(model, con2, x >= x * p) + @test_throws ErrorException set_normalized_coefficient(con, x, 2.0) + set_normalized_coefficient(con1, x, 2.0) + @test_throws ErrorException set_normalized_coefficient(con2, x, 2.0) + return +end + +function test_ListOfConstraintAttributesSet() + cached = MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + MOI.Utilities.AUTOMATIC, + ) + optimizer = POI.Optimizer(cached) + model = direct_model(optimizer) + @variable(model, p in MOI.Parameter(1.0)) + @variable(model, x) + @constraint(model, con, [x * p] in MOI.Nonnegatives(1)) + ret = get_attribute( + model, + MOI.ListOfConstraintAttributesSet{ + MOI.VectorQuadraticFunction{Float64}, + MOI.Nonnegatives, + }(), + ) + @test ret == [] + set_attribute(model, POI._WarnIfQuadraticOfAffineFunctionAmbiguous(), false) + ret = get_attribute( + model, + MOI.ListOfConstraintAttributesSet{ + MOI.VectorQuadraticFunction{Float64}, + MOI.Nonnegatives, }(), ) + @test ret == [] return end