diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 7a717d5d..167124bd 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -7,7 +7,7 @@ function _is_variable(v::MOI.VariableIndex) end function _is_parameter(v::MOI.VariableIndex) - return PARAMETER_INDEX_THRESHOLD < v.value < PARAMETER_INDEX_THRESHOLD_MAX + return PARAMETER_INDEX_THRESHOLD < v.value <= PARAMETER_INDEX_THRESHOLD_MAX end function _has_parameters(f::MOI.ScalarAffineFunction{T}) where {T} @@ -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.", ) @@ -1032,8 +1042,6 @@ function MOI.add_constraint( model.constraint_outer_to_inner[outer_ci] = inner_ci model.quadratic_constraint_cache_set[inner_ci] = set return outer_ci - else - return _add_constraint_direct_and_cache_map!(model, f, set) end return _add_constraint_direct_and_cache_map!(model, f, set) else @@ -1365,16 +1373,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 +1444,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 @@ -1495,8 +1534,12 @@ function MOI.get( return MOI.get(model.optimizer, attr) end -function MOI.supports(model::Optimizer, attr::MOI.AbstractConstraintAttribute) - return MOI.supports(model.optimizer, attr) +function MOI.supports( + model::Optimizer, + attr::MOI.AbstractConstraintAttribute, + ::Type{T}, +) where {T} + return MOI.supports(model.optimizer, attr, T) end function MOI.get( @@ -2115,3 +2158,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 4c08b86d..5c2080ad 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 aa8a6397..89353a29 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 diff --git a/test/moi_tests.jl b/test/moi_tests.jl index 3430d61f..9bae2f2f 100644 --- a/test/moi_tests.jl +++ b/test/moi_tests.jl @@ -2197,3 +2197,193 @@ function test_constrained_variables() MOI.set(optimizer, MOI.ObjectiveFunction{typeof(obj_func)}(), obj_func) return end + +function test_parameter_index_error() + model = POI.Optimizer(MOI.Utilities.Model{Float64}()) + POI._add_variable(model, MOI.VariableIndex(1)) + POI._add_variable(model, MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD)) + @test_throws ErrorException POI._add_variable( + model, + MOI.VariableIndex(POI.PARAMETER_INDEX_THRESHOLD + 1), + ) + @test_throws ErrorException POI._add_variable( + model, + MOI.VariableIndex(typemax(Int64)), + ) + return +end + +struct VariableAttributeForTest <: MOI.AbstractVariableAttribute end +struct ConstraintAttributeForTest <: MOI.AbstractConstraintAttribute end + +function test_variable_attribute_error() + solver = HiGHS.Optimizer() + MOI.set(solver, MOI.Silent(), true) + model = POI.Optimizer( + MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + MOI.Bridges.full_bridge_optimizer(solver, Float64), + ), + ) + x = MOI.add_variable(model) + MOI.set(model, VariableAttributeForTest(), x, 1.0) + p, pc = MOI.add_constrained_variable(model, MOI.Parameter(1.0)) + @test_throws ErrorException MOI.set( + model, + VariableAttributeForTest(), + p, + 1.0, + ) + @test_throws ErrorException MOI.get(model, VariableAttributeForTest(), p) + return +end + +function test_constraint_attribute_error() + solver = MOI.Utilities.Model{Float64}() + model = POI.Optimizer( + MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + MOI.Bridges.full_bridge_optimizer(solver, Float64), + ), + ) + MOI.supports( + model, + ConstraintAttributeForTest(), + MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}, + ) + @test_throws MOI.InvalidIndex MOI.set( + model, + ConstraintAttributeForTest(), + MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}(1), + 1.0, + ) + return +end + +function test_name_from_bound() + solver = MOI.Utilities.Model{Float64}() + model = POI.Optimizer( + MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + MOI.Bridges.full_bridge_optimizer(solver, Float64), + ), + ) + x = MOI.add_variable(model) + p, pc = MOI.add_constrained_variable(model, MOI.Parameter(1.0)) + MOI.set(model, POI.ConstraintsInterpretation(), POI.ONLY_BOUNDS) + ci = MOI.add_constraint(model, x + 1.4 * p, MOI.LessThan(10.0)) + name = MOI.get(model, MOI.ConstraintName(), ci) + @test name == "ParametricBound_MathOptInterface.LessThan{Float64}_" + MOI.set(model, MOI.VariableName(), x, "x_var") + name = MOI.get(model, MOI.ConstraintName(), ci) + @test name == "ParametricBound_MathOptInterface.LessThan{Float64}_x_var" + return +end + +function test_get_constraint_set() + model = POI.Optimizer(MOI.Utilities.Model{Float64}()) + x = MOI.add_variable(model) + p, pc = MOI.add_constrained_variable(model, MOI.Parameter(1.0)) + ci = MOI.add_constraint(model, x + 1.4 * p, MOI.LessThan(10.0)) + set = MOI.get(model, MOI.ConstraintSet(), ci) + @test set isa MOI.LessThan{Float64} + return +end + +function test_quadratic_variable_parameter() + # model = POI.Optimizer(MOI.Utilities.Model{Float64}()) + solver = Ipopt.Optimizer() + MOI.set(solver, MOI.Silent(), true) + model = POI.Optimizer( + MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + MOI.Bridges.full_bridge_optimizer(solver, Float64), + ), + ) + x = MOI.add_variable(model) + p, pc = MOI.add_constrained_variable(model, MOI.Parameter(1.0)) + f = 1.0 * x * x - 2.0 * p * p + ci = MOI.add_constraint(model, MOI.Utilities.vectorize([f]), MOI.Zeros(1)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) + obj_func = 1.0 * x + 2.0 * p + MOI.set(model, MOI.ObjectiveFunction{typeof(obj_func)}(), obj_func) + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.LOCALLY_SOLVED + @test MOI.get(model, MOI.VariablePrimal(), x) ≈ sqrt(2) atol = 1e-4 + MOI.set(model, MOI.ConstraintSet(), pc, MOI.Parameter(2.0)) + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.LOCALLY_SOLVED + @test MOI.get(model, MOI.VariablePrimal(), x) ≈ sqrt(8) atol = 1e-4 + MOI.set(model, MOI.ConstraintSet(), pc, MOI.Parameter(3.0)) + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.LOCALLY_SOLVED + @test MOI.get(model, MOI.VariablePrimal(), x) ≈ sqrt(18) atol = 1e-4 + return +end + +function test_error_modify_quadratic_objective() + model = POI.Optimizer(MOI.Utilities.Model{Float64}()) + x = MOI.add_variable(model) + y = MOI.add_variable(model) + p, pc = MOI.add_constrained_variable(model, MOI.Parameter(1.0)) + quad_func = MOI.ScalarQuadraticFunction( + MOI.ScalarQuadraticTerm.([1.0, 1.0], [x, p], [x, x]), + MOI.ScalarAffineTerm.([2.0, 2.0], [x, y]), + 0.0, + ) + MOI.set( + model, + MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(), + quad_func, + ) + @test_throws ErrorException MOI.modify( + model, + MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(), + MOI.ScalarConstantChange{Float64}(1.1), + ) + @test_throws ErrorException MOI.modify( + model, + MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(), + MOI.ScalarCoefficientChange{Float64}(x, 2.2), + ) + return +end + +function test_list_of_parametric_constraint_types_present_conflict() + inner = MOI.Utilities.Model{Float64}() + mock = MOI.Utilities.MockOptimizer(inner; supports_names = false) + model = POI.Optimizer(MOI.Bridges.full_bridge_optimizer(mock, Float64)) + x = MOI.add_variable(model) + x_ci = MOI.add_constrained_variable(model, MOI.Parameter(2.0)) + p, p_ci = MOI.add_constrained_variable(model, MOI.Parameter(2.0)) + f = MOI.Utilities.vectorize([1.0 * x + 2.0 * p]) + ci = MOI.add_constraint(model, f, MOI.Nonpositives(1)) + @test MOI.get(model, POI.ListOfParametricConstraintTypesPresent()) == + Tuple{DataType,DataType,DataType}[( + MOI.VectorAffineFunction{Float64}, + MOI.Nonpositives, + POI.ParametricVectorAffineFunction{Float64}, + )] + return +end + +function test_get_constraint_function_vector() + model = POI.Optimizer(MOI.Utilities.Model{Float64}()) + x = MOI.add_variable(model) + p, pc = MOI.add_constrained_variable(model, MOI.Parameter(1.0)) + f = 1.0 * x * x - 2.0 * p * p + ci = MOI.add_constraint(model, MOI.Utilities.vectorize([f]), MOI.Zeros(1)) + f2 = MOI.get(model, MOI.ConstraintFunction(), ci) + @test canonical_compare(MOI.Utilities.vectorize([f]), f2) + return +end + +function test_multiplicative_dual_error() + model = POI.Optimizer(MOI.Utilities.Model{Float64}()) + x = MOI.add_variable(model) + p, pc = MOI.add_constrained_variable(model, MOI.Parameter(1.0)) + f = 1.0 * x * p + ci = MOI.add_constraint(model, f, MOI.EqualTo{Float64}(0.0)) + @test_throws ErrorException MOI.get(model, MOI.ConstraintDual(), pc) + return +end