diff --git a/docs/src/apireference.md b/docs/src/apireference.md index 2bdb4f5b46..f6a069f230 100644 --- a/docs/src/apireference.md +++ b/docs/src/apireference.md @@ -254,6 +254,8 @@ Functions for getting properties of sets. ```@docs dimension constant(s::EqualTo) +supports_dimension_update +update_dimension ``` ### Scalar sets @@ -612,6 +614,41 @@ Utilities.state Utilities.mode ``` +## Function utilities + +The following utilities are available for functions: +```@docs +Utilities.eval_variables +Utilities.remove_variable +Utilities.all_coefficients +Utilities.unsafe_add +Utilities.isapprox_zero +Utilities.modify_function +``` + +The following functions can be used to canonicalize a function: +```@docs +Utilities.is_canonical +Utilities.canonical +Utilities.canonicalize! +``` + +The following functions can be used to manipulate functions with basic algebra: +```@docs +Utilities.scalar_type +Utilities.promote_operation +Utilities.operate +Utilities.operate! +Utilities.vectorize +``` + +## Set utilities + +The following utilities are available for sets: +```@docs +Utilities.shift_constant +``` + ## Benchmarks Functions to help benchmark the performance of solver wrappers. See diff --git a/src/Bridges/Constraint/slack.jl b/src/Bridges/Constraint/slack.jl index 0d681aaa72..4b887ca1b7 100644 --- a/src/Bridges/Constraint/slack.jl +++ b/src/Bridges/Constraint/slack.jl @@ -93,7 +93,7 @@ end function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintFunction, b::ScalarSlackBridge{T}) where T - return MOIU.removevariable(MOI.get(model, attr, b.equality), b.slack) + return MOIU.remove_variable(MOI.get(model, attr, b.equality), b.slack) end function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintSet, b::ScalarSlackBridge) @@ -191,7 +191,7 @@ end function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintFunction, b::VectorSlackBridge{T}) where T - return MOIU.removevariable(MOI.get(model, attr, b.equality), b.slacks) + return MOIU.remove_variable(MOI.get(model, attr, b.equality), b.slacks) end function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintSet, b::VectorSlackBridge) diff --git a/src/Test/modellike.jl b/src/Test/modellike.jl index 88494d0057..7c4853df2e 100644 --- a/src/Test/modellike.jl +++ b/src/Test/modellike.jl @@ -471,3 +471,95 @@ function set_upper_bound_twice(model::MOI.ModelLike, T::Type) MOI.delete(model, ci) end end + +function delete_test(model::MOI.ModelLike) + x = MOI.add_variable(model) + cx = MOI.add_constraint(model, x, MOI.GreaterThan(0.0)) + y = MOI.add_variables(model, 4) + cy = MOI.add_constraint(model, y, MOI.Nonpositives(4)) + @test MOI.is_valid(model, x) + @test MOI.is_valid(model, y[1]) + @test MOI.is_valid(model, y[2]) + @test MOI.is_valid(model, y[3]) + @test MOI.is_valid(model, y[4]) + @test MOI.is_valid(model, cx) + @test MOI.is_valid(model, cy) + @test MOI.get(model, MOI.ConstraintFunction(), cx) == MOI.SingleVariable(x) + @test MOI.get(model, MOI.ConstraintSet(), cx) == MOI.GreaterThan(0.0) + @test MOI.get(model, MOI.ConstraintFunction(), cy) == MOI.VectorOfVariables(y) + @test MOI.get(model, MOI.ConstraintSet(), cy) == MOI.Nonpositives(4) + @test Set(MOI.get(model, MOI.ListOfConstraints())) == + Set([(MOI.SingleVariable, MOI.GreaterThan{Float64}), + (MOI.VectorOfVariables, MOI.Nonpositives)]) + @test MOI.get(model, MOI.ListOfConstraintIndices{ + MOI.SingleVariable, MOI.GreaterThan{Float64}}()) == [cx] + @test MOI.get(model, MOI.ListOfConstraintIndices{ + MOI.VectorOfVariables, MOI.Nonpositives}()) == [cy] + MOI.delete(model, y[3]) + @test MOI.is_valid(model, x) + @test MOI.is_valid(model, y[1]) + @test MOI.is_valid(model, y[2]) + @test !MOI.is_valid(model, y[3]) + @test MOI.is_valid(model, y[4]) + @test MOI.is_valid(model, cx) + @test MOI.is_valid(model, cy) + @test MOI.get(model, MOI.ConstraintFunction(), cx) == MOI.SingleVariable(x) + @test MOI.get(model, MOI.ConstraintSet(), cx) == MOI.GreaterThan(0.0) + @test MOI.get(model, MOI.ConstraintFunction(), cy) == MOI.VectorOfVariables(y[[1, 2, 4]]) + @test MOI.get(model, MOI.ConstraintSet(), cy) == MOI.Nonpositives(3) + @test Set(MOI.get(model, MOI.ListOfConstraints())) == + Set([(MOI.SingleVariable, MOI.GreaterThan{Float64}), + (MOI.VectorOfVariables, MOI.Nonpositives)]) + @test MOI.get(model, MOI.ListOfConstraintIndices{ + MOI.SingleVariable, MOI.GreaterThan{Float64}}()) == [cx] + @test MOI.get(model, MOI.ListOfConstraintIndices{ + MOI.VectorOfVariables, MOI.Nonpositives}()) == [cy] + MOI.delete(model, y[1]) + @test MOI.is_valid(model, x) + @test !MOI.is_valid(model, y[1]) + @test MOI.is_valid(model, y[2]) + @test !MOI.is_valid(model, y[3]) + @test MOI.is_valid(model, y[4]) + @test MOI.is_valid(model, cx) + @test MOI.is_valid(model, cy) + @test MOI.get(model, MOI.ConstraintFunction(), cx) == MOI.SingleVariable(x) + @test MOI.get(model, MOI.ConstraintSet(), cx) == MOI.GreaterThan(0.0) + @test MOI.get(model, MOI.ConstraintFunction(), cy) == MOI.VectorOfVariables(y[[2, 4]]) + @test MOI.get(model, MOI.ConstraintSet(), cy) == MOI.Nonpositives(2) + @test Set(MOI.get(model, MOI.ListOfConstraints())) == + Set([(MOI.SingleVariable, MOI.GreaterThan{Float64}), + (MOI.VectorOfVariables, MOI.Nonpositives)]) + @test MOI.get(model, MOI.ListOfConstraintIndices{ + MOI.SingleVariable, MOI.GreaterThan{Float64}}()) == [cx] + @test MOI.get(model, MOI.ListOfConstraintIndices{ + MOI.VectorOfVariables, MOI.Nonpositives}()) == [cy] + MOI.delete(model, x) + @test !MOI.is_valid(model, x) + @test !MOI.is_valid(model, y[1]) + @test MOI.is_valid(model, y[2]) + @test !MOI.is_valid(model, y[3]) + @test MOI.is_valid(model, y[4]) + @test !MOI.is_valid(model, cx) + @test MOI.is_valid(model, cy) + @test MOI.get(model, MOI.ConstraintFunction(), cy) == MOI.VectorOfVariables(y[[2, 4]]) + @test MOI.get(model, MOI.ConstraintSet(), cy) == MOI.Nonpositives(2) + @test MOI.get(model, MOI.ListOfConstraints()) == + [(MOI.VectorOfVariables, MOI.Nonpositives)] + @test isempty(MOI.get(model, MOI.ListOfConstraintIndices{ + MOI.SingleVariable, MOI.GreaterThan{Float64}}())) + @test MOI.get(model, MOI.ListOfConstraintIndices{ + MOI.VectorOfVariables, MOI.Nonpositives}()) == [cy] + MOI.delete(model, y[[2, 4]]) + @test !MOI.is_valid(model, x) + @test !MOI.is_valid(model, y[1]) + @test !MOI.is_valid(model, y[2]) + @test !MOI.is_valid(model, y[3]) + @test !MOI.is_valid(model, y[4]) + @test !MOI.is_valid(model, cx) + @test !MOI.is_valid(model, cy) + @test isempty(MOI.get(model, MOI.ListOfConstraints())) + @test isempty(MOI.get(model, MOI.ListOfConstraintIndices{ + MOI.SingleVariable, MOI.GreaterThan{Float64}}())) + @test isempty(MOI.get(model, MOI.ListOfConstraintIndices{ + MOI.VectorOfVariables, MOI.Nonpositives}())) +end diff --git a/src/Utilities/functions.jl b/src/Utilities/functions.jl index b1a1ac7726..b44d010aa1 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -1,30 +1,30 @@ using Test """ - evalvariables(varval::Function, f::AbstractFunction) + eval_variables(varval::Function, f::AbstractFunction) Returns the value of function `f` if each variable index `vi` is evaluated as `varval(vi)`. """ -function evalvariables end -evalvariables(varval::Function, f::SVF) = varval(f.variable) -evalvariables(varval::Function, f::VVF) = varval.(f.variables) -function evalvariables(varval::Function, f::SAF) +function eval_variables end +eval_variables(varval::Function, f::SVF) = varval(f.variable) +eval_variables(varval::Function, f::VVF) = varval.(f.variables) +function eval_variables(varval::Function, f::SAF) return mapreduce(t->evalterm(varval, t), +, f.terms, init=f.constant) end -function evalvariables(varval::Function, f::VAF) +function eval_variables(varval::Function, f::VAF) out = copy(f.constants) for t in f.terms out[t.output_index] += evalterm(varval, t.scalar_term) end out end -function evalvariables(varval::Function, f::SQF) +function eval_variables(varval::Function, f::SQF) init = zero(f.constant) lin = mapreduce(t->evalterm(varval, t), +, f.affine_terms, init=init) quad = mapreduce(t->evalterm(varval, t), +, f.quadratic_terms, init=init) return lin + quad + f.constant end -function evalvariables(varval::Function, f::VQF) +function eval_variables(varval::Function, f::VQF) out = copy(f.constants) for t in f.affine_terms out[t.output_index] += evalterm(varval, t.scalar_term) @@ -204,13 +204,13 @@ function unsafe_add(t1::VT, t2::VT) where VT <: Union{MOI.VectorAffineTerm, end """ - iscanonical(f::Union{ScalarAffineFunction, ScalarQuadraticFunction + is_canonical(f::Union{ScalarAffineFunction, ScalarQuadraticFunction VectorAffineFunction, VectorQuadraticTerm}) Returns a Bool indicating whether the function is in canonical form. See [`canonical`](@ref). """ -function iscanonical(f::Union{SAF, VAF, SQF, VQF}) +function is_canonical(f::Union{SAF, VAF, SQF, VQF}) is_strictly_sorted(f.terms, MOI.term_indices, t -> !iszero(MOI.coefficient(t))) end @@ -460,44 +460,44 @@ function _rmvar(vis_or_terms::Vector, vi) end """ - removevariable(f::AbstractFunction, vi::VariableIndex) + remove_variable(f::AbstractFunction, vi::VariableIndex) Return a new function `f` with the variable vi removed. """ -function removevariable end -function removevariable(f::MOI.SingleVariable, vi::MOI.VariableIndex) +function remove_variable end +function remove_variable(f::MOI.SingleVariable, vi::MOI.VariableIndex) if f.variable == vi error("Cannot remove variable from a `SingleVariable` function of the", " same variable.") end return f end -function removevariable(f::VVF, vi) +function remove_variable(f::VVF, vi) VVF(_rmvar(f.variables, vi)) end -function removevariable(f::Union{SAF, VAF}, vi) +function remove_variable(f::Union{SAF, VAF}, vi) typeof(f)(_rmvar(f.terms, vi), MOI.constant(f)) end -function removevariable(f::Union{SQF, VQF}, vi) +function remove_variable(f::Union{SQF, VQF}, vi) terms = _rmvar.((f.affine_terms, f.quadratic_terms), Ref(vi)) typeof(f)(terms..., MOI.constant(f)) end """ - modifyfunction(f::AbstractFunction, change::AbstractFunctionModification) + modify_function(f::AbstractFunction, change::AbstractFunctionModification) Return a new function `f` modified according to `change`. """ -function modifyfunction(f::SAF, change::MOI.ScalarConstantChange) +function modify_function(f::SAF, change::MOI.ScalarConstantChange) return SAF(f.terms, change.new_constant) end -function modifyfunction(f::VAF, change::MOI.VectorConstantChange) +function modify_function(f::VAF, change::MOI.VectorConstantChange) return VAF(f.terms, change.new_constant) end -function modifyfunction(f::SQF, change::MOI.ScalarConstantChange) +function modify_function(f::SQF, change::MOI.ScalarConstantChange) return SQF(f.affine_terms, f.quadratic_terms, change.new_constant) end -function modifyfunction(f::VQF, change::MOI.VectorConstantChange) +function modify_function(f::VQF, change::MOI.VectorConstantChange) return VQF(f.affine_terms, f.quadratic_terms, change.new_constant) end function _modifycoefficient(terms::Vector{<:MOI.ScalarAffineTerm}, variable::VI, new_coefficient) @@ -518,11 +518,11 @@ function _modifycoefficient(terms::Vector{<:MOI.ScalarAffineTerm}, variable::VI, end terms end -function modifyfunction(f::SAF, change::MOI.ScalarCoefficientChange) +function modify_function(f::SAF, change::MOI.ScalarCoefficientChange) lin = _modifycoefficient(f.terms, change.variable, change.new_coefficient) return SAF(lin, f.constant) end -function modifyfunction(f::SQF, change::MOI.ScalarCoefficientChange) +function modify_function(f::SQF, change::MOI.ScalarCoefficientChange) lin = _modifycoefficient(f.affine_terms, change.variable, change.new_coefficient) return SQF(lin, f.quadratic_terms, f.constant) end @@ -553,13 +553,13 @@ function _modifycoefficients(n, terms::Vector{<:MOI.VectorAffineTerm}, variable: end terms end -function modifyfunction(f::VAF, change::MOI.MultirowChange) +function modify_function(f::VAF, change::MOI.MultirowChange) dim = MOI.output_dimension(f) coefficients = change.new_coefficients lin = _modifycoefficients(dim, f.terms, change.variable, coefficients) VAF(lin, f.constants) end -function modifyfunction(f::VQF, change::MOI.MultirowChange) +function modify_function(f::VQF, change::MOI.MultirowChange) dim = MOI.output_dimension(f) coefficients = change.new_coefficients lin = _modifycoefficients(dim, f.affine_terms, change.variable, coefficients) diff --git a/src/Utilities/mockoptimizer.jl b/src/Utilities/mockoptimizer.jl index 73287726b1..0af8212893 100644 --- a/src/Utilities/mockoptimizer.jl +++ b/src/Utilities/mockoptimizer.jl @@ -385,6 +385,19 @@ function MOI.delete(mock::MockOptimizer, index::MOI.VariableIndex) MOI.delete(mock.inner_model, xor_index(index)) delete!(mock.varprimal, index) end +function MOI.delete(mock::MockOptimizer, indices::Vector{MOI.VariableIndex}) + if !mock.delete_allowed && !isempty(indices) + throw(MOI.DeleteNotAllowed(first(indices))) + end + for index in indices + # The index thrown by `mock.inner_model` would be xored + MOI.throw_if_not_valid(mock, index) + end + MOI.delete(mock.inner_model, xor_index.(indices)) + for index in indices + delete!(mock.varprimal, index) + end +end function MOI.delete(mock::MockOptimizer, index::MOI.ConstraintIndex) if !mock.delete_allowed throw(MOI.DeleteNotAllowed(index)) diff --git a/src/Utilities/model.jl b/src/Utilities/model.jl index 9d69737826..165ebe50be 100644 --- a/src/Utilities/model.jl +++ b/src/Utilities/model.jl @@ -49,7 +49,7 @@ end _modifyconstr(ci::CI{F, S}, f::F, s::S, change::F) where {F, S} = (ci, change, s) _modifyconstr(ci::CI{F, S}, f::F, s::S, change::S) where {F, S} = (ci, f, change) -_modifyconstr(ci::CI{F, S}, f::F, s::S, change::MOI.AbstractFunctionModification) where {F, S} = (ci, modifyfunction(f, change), s) +_modifyconstr(ci::CI{F, S}, f::F, s::S, change::MOI.AbstractFunctionModification) where {F, S} = (ci, modify_function(f, change), s) function _modify(constrs::Vector{ConstraintEntry{F, S}}, ci::CI{F}, i::Int, change) where {F, S} constrs[i] = _modifyconstr(constrs[i]..., change) @@ -103,7 +103,7 @@ function MOI.add_variables(model::AbstractModel, n::Integer) end """ - removevariable(f::MOI.AbstractFunction, s::MOI.AbstractSet, vi::MOI.VariableIndex) + remove_variable(f::MOI.AbstractFunction, s::MOI.AbstractSet, vi::MOI.VariableIndex) Return a tuple `(g, t)` representing the constraint `f`-in-`s` with the variable `vi` removed. That is, the terms containing the variable `vi` in the @@ -111,46 +111,74 @@ function `f` are removed and the dimension of the set `s` is updated if needed (e.g. when `f` is a `VectorOfVariables` with `vi` being one of the variables). """ -removevariable(f, s, vi::VI) = removevariable(f, vi), s -function removevariable(f::MOI.VectorOfVariables, s, vi::VI) - g = removevariable(f, vi) +remove_variable(f, s, vi::VI) = remove_variable(f, vi), s +function remove_variable(f::MOI.VectorOfVariables, s, vi::VI) + g = remove_variable(f, vi) if length(g.variables) != length(f.variables) - t = updatedimension(s, length(g.variables)) + t = MOI.update_dimension(s, length(g.variables)) else t = s end return g, t end -function _removevar!(constrs::Vector, vi::VI) +function _remove_variable(constrs::Vector, vi::VI) for i in eachindex(constrs) ci, f, s = constrs[i] - constrs[i] = (ci, removevariable(f, s, vi)...) + constrs[i] = (ci, remove_variable(f, s, vi)...) end return CI{MOI.SingleVariable}[] end -function _removevar!(constrs::Vector{<:ConstraintEntry{MOI.SingleVariable}}, - vi::VI) - # If a variable is removed, the SingleVariable constraints using this variable - # need to be removed too - rm = CI{MOI.SingleVariable}[] +function _vector_of_variables_with(::Vector, ::Union{VI, MOI.Vector{VI}}) + return CI{MOI.VectorOfVariables}[] +end +function throw_delete_variable_in_vov(vi::VI) + message = string("Cannot delete variable as it is constrained with other", + " variables in a `MOI.VectorOfVariables`.") + throw(MOI.DeleteNotAllowed(vi, message)) +end +function _vector_of_variables_with( + constrs::Vector{<:ConstraintEntry{MOI.VectorOfVariables}}, vi::VI) + rm = CI{MOI.VectorOfVariables}[] for (ci, f, s) in constrs - if f.variable == vi - push!(rm, ci) + if vi in f.variables + if length(f.variables) > 1 + # If `supports_dimension_update(s)` then the variable will be + # removed in `_remove_variable`. + if !MOI.supports_dimension_update(typeof(s)) + throw_delete_variable_in_vov(vi) + end + else + push!(rm, ci) + end end end return rm end -function MOI.delete(model::AbstractModel, vi::VI) - if !MOI.is_valid(model, vi) - throw(MOI.InvalidIndex(vi)) +function _vector_of_variables_with( + constrs::Vector{<:ConstraintEntry{MOI.VectorOfVariables}}, + vis::Vector{VI} +) + rm = CI{MOI.VectorOfVariables}[] + for (ci, f, s) in constrs + if vis == f.variables + push!(rm, ci) + end end - model.objective = removevariable(model.objective, vi) - # `ci_to_remove` is the list of indices of the `SingleVariable` constraints - # of `vi` - ci_to_remove = broadcastvcat(constrs -> _removevar!(constrs, vi), model) - for ci in ci_to_remove + return rm +end +function MOI.delete(model::AbstractModel{T}, vi::VI) where T + MOI.throw_if_not_valid(model, vi) + model.objective = remove_variable(model.objective, vi) + # If a variable is removed, the `VectorOfVariables` constraints using this + # variable only need to be removed too. `vov_to_remove` is the list of + # indices of the `VectorOfVariables` constraints of `vi`. + vov_to_remove = broadcastvcat(constrs -> _vector_of_variables_with(constrs, vi), model) + for ci in vov_to_remove MOI.delete(model, ci) end + # `VectorOfVariables` constraints with sets not supporting dimension update + # were either deleted or an error was thrown. The rest is modified now. + broadcastcall(constrs -> _remove_variable(constrs, vi), model) model.single_variable_mask[vi.value] = 0x0 if model.variable_indices === nothing model.variable_indices = Set(MOI.get(model, @@ -158,8 +186,26 @@ function MOI.delete(model::AbstractModel, vi::VI) end delete!(model.variable_indices, vi) model.name_to_var = nothing - if haskey(model.var_to_name, vi) - delete!(model.var_to_name, vi) + delete!(model.var_to_name, vi) + model.name_to_con = nothing + delete!(model.con_to_name, MOI.ConstraintIndex{MOI.SingleVariable, MOI.EqualTo{T}}(vi.value)) + delete!(model.con_to_name, MOI.ConstraintIndex{MOI.SingleVariable, MOI.GreaterThan{T}}(vi.value)) + delete!(model.con_to_name, MOI.ConstraintIndex{MOI.SingleVariable, MOI.LessThan{T}}(vi.value)) + delete!(model.con_to_name, MOI.ConstraintIndex{MOI.SingleVariable, MOI.Interval{T}}(vi.value)) + delete!(model.con_to_name, MOI.ConstraintIndex{MOI.SingleVariable, MOI.Integer}(vi.value)) + delete!(model.con_to_name, MOI.ConstraintIndex{MOI.SingleVariable, MOI.ZeroOne}(vi.value)) + delete!(model.con_to_name, MOI.ConstraintIndex{MOI.SingleVariable, MOI.Semicontinuous{T}}(vi.value)) + delete!(model.con_to_name, MOI.ConstraintIndex{MOI.SingleVariable, MOI.Semiinteger{T}}(vi.value)) +end +function MOI.delete(model::AbstractModel, vis::Vector{VI}) + # Delete `VectorOfVariables(vis)` constraints as otherwise, it will error + # when removing variables one by one. + vov_to_remove = broadcastvcat(constrs -> _vector_of_variables_with(constrs, vis), model) + for ci in vov_to_remove + MOI.delete(model, ci) + end + for vi in vis + MOI.delete(model, vi) end end @@ -214,26 +260,48 @@ function MOI.set(model::AbstractModel, ::MOI.VariableName, vi::VI, name::String) end MOI.get(model::AbstractModel, ::MOI.VariableName, vi::VI) = get(model.var_to_name, vi, EMPTYSTRING) +""" + build_name_to_var_map(con_to_name::Dict{MOI.VariableIndex, String}) + +Create and return a reverse map from name to variable index, given a map from +variable index to name. The special value `MOI.VariableIndex(0)` is used to +indicate that multiple variables have the same name. +""" +function build_name_to_var_map(var_to_name::Dict{VI, String}) + name_to_var = Dict{String, VI}() + for (var, var_name) in var_to_name + if haskey(name_to_var, var_name) + # 0 is a special value that means this string does not map to + # a unique variable name. + name_to_var[var_name] = VI(0) + else + name_to_var[var_name] = var + end + end + return name_to_var +end + +function throw_multiple_name_error(::Type{MOI.VariableIndex}, name::String) + error("Multiple variables have the name $name.") +end +function throw_multiple_name_error(::Type{<:MOI.ConstraintIndex}, name::String) + error("Multiple constraints have the name $name.") +end +function throw_if_multiple_with_name(::Nothing, ::String) end +function throw_if_multiple_with_name(index::MOI.Index, name::String) + if iszero(index.value) + throw_multiple_name_error(typeof(index), name) + end +end + function MOI.get(model::AbstractModel, ::Type{VI}, name::String) if model.name_to_var === nothing # Rebuild the map. - model.name_to_var = Dict{String, VI}() - for (var, var_name) in model.var_to_name - if haskey(model.name_to_var, var_name) - # -1 is a special value that means this string does not map to - # a unique variable name. - model.name_to_var[var_name] = VI(-1) - else - model.name_to_var[var_name] = var - end - end + model.name_to_var = build_name_to_var_map(model.var_to_name) end result = get(model.name_to_var, name, nothing) - if result == VI(-1) - error("Multiple variables have the name $name.") - else - return result - end + throw_if_multiple_with_name(result, name) + return result end function MOI.get(model::AbstractModel, ::MOI.ListOfVariableAttributesSet)::Vector{MOI.AbstractVariableAttribute} @@ -252,14 +320,14 @@ MOI.get(model::AbstractModel, ::MOI.ConstraintName, ci::CI) = get(model.con_to_n Create and return a reverse map from name to constraint index, given a map from constraint index to name. The special value -`MOI.ConstraintIndex{Nothing, Nothing}(-1)` is used to indicate that multiple +`MOI.ConstraintIndex{Nothing, Nothing}(0)` is used to indicate that multiple constraints have the same name. """ function build_name_to_con_map(con_to_name::Dict{CI, String}) name_to_con = Dict{String, CI}() for (con, con_name) in con_to_name if haskey(name_to_con, con_name) - name_to_con[con_name] = CI{Nothing, Nothing}(-1) + name_to_con[con_name] = CI{Nothing, Nothing}(0) else name_to_con[con_name] = con end @@ -274,9 +342,8 @@ function MOI.get(model::AbstractModel, ConType::Type{<:CI}, name::String) model.name_to_con = build_name_to_con_map(model.con_to_name) end ci = get(model.name_to_con, name, nothing) - if ci == CI{Nothing, Nothing}(-1) - error("Multiple constraints have the name $name.") - elseif ci isa ConType + throw_if_multiple_with_name(ci, name) + if ci isa ConType return ci else return nothing @@ -308,7 +375,7 @@ function MOI.set(model::AbstractModel, ::MOI.ObjectiveFunction, f::MOI.AbstractF end function MOI.modify(model::AbstractModel, obj::MOI.ObjectiveFunction, change::MOI.AbstractFunctionModification) - model.objective = modifyfunction(model.objective, change) + model.objective = modify_function(model.objective, change) end MOI.get(::AbstractModel, ::MOI.ListOfOptimizerAttributesSet) = MOI.AbstractOptimizerAttribute[] @@ -335,6 +402,8 @@ single_variable_flag(::Type{MOI.Integer}) = 0x10 single_variable_flag(::Type{MOI.ZeroOne}) = 0x20 single_variable_flag(::Type{<:MOI.Semicontinuous}) = 0x40 single_variable_flag(::Type{<:MOI.Semiinteger}) = 0x80 +# If a set is added here, a line should be added in +# `MOI.delete(::AbstractModel, ::MOI.VariableIndex)` function flag_to_set_type(flag::UInt8, T::Type) if flag == 0x1 @@ -446,14 +515,10 @@ function _delete_constraint(model::AbstractModel, ci::CI) model.constrmap[ci.value] = 0 end function MOI.delete(model::AbstractModel, ci::CI) - if !MOI.is_valid(model, ci) - throw(MOI.InvalidIndex(ci)) - end + MOI.throw_if_not_valid(model, ci) _delete_constraint(model, ci) model.name_to_con = nothing - if haskey(model.con_to_name, ci) - delete!(model.con_to_name, ci) - end + delete!(model.con_to_name, ci) end function MOI.modify(model::AbstractModel, ci::CI, change::MOI.AbstractFunctionModification) diff --git a/src/Utilities/results.jl b/src/Utilities/results.jl index a6662305bc..2d17de6734 100644 --- a/src/Utilities/results.jl +++ b/src/Utilities/results.jl @@ -20,7 +20,7 @@ function get_fallback(model::MOI.ModelLike, ::MOI.ObjectiveValue) F = MOI.get(model, MOI.ObjectiveFunctionType()) f = MOI.get(model, MOI.ObjectiveFunction{F}()) # TODO do not include constant if primal solution is a ray - return evalvariables(vi -> MOI.get(model, MOI.VariablePrimal(), vi), f) + return eval_variables(vi -> MOI.get(model, MOI.VariablePrimal(), vi), f) end function constraint_constant(model::MOI.ModelLike, @@ -149,7 +149,7 @@ function get_fallback(model::MOI.ModelLike, ::MOI.ConstraintPrimal, idx::MOI.ConstraintIndex) f = MOI.get(model, MOI.ConstraintFunction(), idx) # TODO do not include constant if primal solution is a ray - return evalvariables(vi -> MOI.get(model, MOI.VariablePrimal(), vi), f) + return eval_variables(vi -> MOI.get(model, MOI.VariablePrimal(), vi), f) end ################ Constraint Dual for Variable-wise constraints ################# diff --git a/src/Utilities/sets.jl b/src/Utilities/sets.jl index 9b526f66d5..f23ed8c604 100644 --- a/src/Utilities/sets.jl +++ b/src/Utilities/sets.jl @@ -1,16 +1,3 @@ -const DimensionUpdatableSets = Union{MOI.Reals, - MOI.Zeros, - MOI.Nonnegatives, - MOI.Nonpositives} -""" - updatedimension(s::AbstractVectorSet, newdim) - -Returns a set with the dimension modified to `newdim`. -""" -function updatedimension(::S, newdim) where S<:DimensionUpdatableSets - S(newdim) -end - """ shift_constant(set::MOI.AbstractScalarSet, offset) diff --git a/src/Utilities/universalfallback.jl b/src/Utilities/universalfallback.jl index 46ce06ace4..c548e92a58 100644 --- a/src/Utilities/universalfallback.jl +++ b/src/Utilities/universalfallback.jl @@ -73,30 +73,86 @@ function MOI.delete(uf::UniversalFallback, ci::CI{F, S}) where {F, S} throw(MOI.InvalidIndex(ci)) end delete!(uf.constraints[(F, S)], ci) - if haskey(uf.con_to_name, ci) - delete!(uf.con_to_name, ci) - end + delete!(uf.con_to_name, ci) uf.name_to_con = nothing end for d in values(uf.conattr) delete!(d, ci) end end +function _remove_variable(uf::UniversalFallback, + constraints::Dict{<:CI{MOI.SingleVariable}}, vi::VI) + to_delete = keytype(constraints)[] + for (ci, constraint) in constraints + f::MOI.SingleVariable = constraint[1] + if f.variable == vi + push!(to_delete, ci) + end + end + MOI.delete(uf, to_delete) +end +function _remove_variable(uf::UniversalFallback, + constraints::Dict{CI{MOI.VectorOfVariables, S}}, + vi::VI) where S + to_delete = keytype(constraints)[] + for (ci, constraint) in constraints + f::MOI.VectorOfVariables, s = constraint + if vi in f.variables + if length(f.variables) > 1 + if MOI.supports_dimension_update(S) + constraints[ci] = remove_variable(f, s, vi) + else + throw_delete_variable_in_vov(vi) + end + else + push!(to_delete, ci) + end + end + end + MOI.delete(uf, to_delete) +end +function _remove_variable(::UniversalFallback, constraints::Dict{<:CI}, vi::VI) + for (ci, constraint) in constraints + f, s = constraint + constraints[ci] = remove_variable(f, s, vi) + end +end +function _remove_vector_of_variables( + uf::UniversalFallback, constraints::Dict{<:CI{MOI.VectorOfVariables}}, + vis::Vector{VI} +) + to_delete = keytype(constraints)[] + for (ci, constraint) in constraints + f::MOI.VectorOfVariables = constraint[1] + if vis == f.variables + push!(to_delete, ci) + end + end + MOI.delete(uf, to_delete) +end +function _remove_vector_of_variables( + ::UniversalFallback, ::Dict{<:CI}, ::Vector{VI}) +end function MOI.delete(uf::UniversalFallback, vi::VI) MOI.delete(uf.model, vi) for d in values(uf.varattr) delete!(d, vi) end - for (FS, constraints) in uf.constraints - for (ci, constraint) in constraints - f, s = constraint - if f isa MOI.SingleVariable - if f.variable == vi - delete!(constraints, ci) - end - else - constraints[ci] = removevariable(f, s, vi) - end + for (_, constraints) in uf.constraints + _remove_variable(uf, constraints, vi) + end +end +function MOI.delete(uf::UniversalFallback, vis::Vector{VI}) + MOI.delete(uf.model, vis) + for d in values(uf.varattr) + for vi in vis + delete!(d, vi) + end + end + for (_, constraints) in uf.constraints + _remove_vector_of_variables(uf, constraints, vis) + for vi in vis + _remove_variable(uf, constraints, vi) end end end @@ -214,47 +270,38 @@ function MOI.get(uf::UniversalFallback, attr::MOI.ConstraintName, ci::CI{F, S}) end MOI.get(uf::UniversalFallback, ::Type{VI}, name::String) = MOI.get(uf.model, VI, name) + +check_type_and_multiple_names(::Type, ::Nothing, ::Nothing, name) = nothing +check_type_and_multiple_names(::Type{T}, value::T, ::Nothing, name) where T = value +check_type_and_multiple_names(::Type, ::Any, ::Nothing, name) where T = nothing +check_type_and_multiple_names(::Type{T}, ::Nothing, value::T, name) where T = value +check_type_and_multiple_names(::Type, ::Nothing, ::Any, name) where T = nothing +function check_type_and_multiple_names(T::Type, ::Any, ::Any, name) + throw_multiple_name_error(T, name) +end function MOI.get(uf::UniversalFallback, ::Type{CI{F, S}}, name::String) where {F, S} if uf.name_to_con === nothing uf.name_to_con = build_name_to_con_map(uf.con_to_name) end - if MOI.supports_constraint(uf.model, F, S) ci = MOI.get(uf.model, CI{F, S}, name) - if ci !== nothing && haskey(uf.name_to_con, name) - error("Multiple constraints have the name $name.") - end - return ci else - ci = get(uf.name_to_con, name, nothing) - if ci == CI{Nothing, Nothing}(-1) - error("Multiple constraints have the name $name.") - elseif ci isa CI{F, S} - return ci - else - return nothing - end + # There is no `F`-in-`S` constraint in `b.model`, `ci` is only queried + # to check for duplicate names. + ci = MOI.get(uf.model, CI, name) end + ci_uf = get(uf.name_to_con, name, nothing) + throw_if_multiple_with_name(ci_uf, name) + return check_type_and_multiple_names(CI{F, S}, ci_uf, ci, name) end function MOI.get(uf::UniversalFallback, ::Type{CI}, name::String) if uf.name_to_con === nothing uf.name_to_con = build_name_to_con_map(uf.con_to_name) end - - ci = MOI.get(uf.model, CI, name) - if ci === nothing - uf_ci = get(uf.name_to_con, name, nothing) - if uf_ci == CI{Nothing, Nothing}(-1) - error("Multiple constraints have the name $name.") - else - return uf_ci - end - else - if haskey(uf.name_to_con, name) - error("Multiple constraints have the name $name.") - end - return ci - end + ci_uf = get(uf.name_to_con, name, nothing) + throw_if_multiple_with_name(ci_uf, name) + return check_type_and_multiple_names( + CI, ci_uf, MOI.get(uf.model, CI, name), name) end _set(uf, attr::MOI.AbstractOptimizerAttribute, value) = uf.optattr[attr] = value @@ -323,7 +370,7 @@ function MOI.modify(uf::UniversalFallback, ci::CI{F, S}, change::MOI.AbstractFun MOI.modify(uf.model, ci, change) else (f, s) = uf.constraints[(F, S)][ci] - uf.constraints[(F, S)][ci] = (modifyfunction(f, change), s) + uf.constraints[(F, S)][ci] = (modify_function(f, change), s) end end diff --git a/src/indextypes.jl b/src/indextypes.jl index 8954a64506..9e82465f24 100644 --- a/src/indextypes.jl +++ b/src/indextypes.jl @@ -95,7 +95,23 @@ end """ delete(model::ModelLike, index::Index) -Delete the referenced object from the model. +Delete the referenced object from the model. Throw [`DeleteNotAllowed`](@ref) if +if `index` cannot be deleted. + +The following modifications also take effect if `Index` is [`VariableIndex`](@ref): +* If `index` used in the objective function, it is removed from the function, + i.e., it is substituted for zero. +* For each `func`-in-`set` constraint of the model: + - If `func isa SingleVariable` and `func.variable == index` then the + constraint is deleted. + - If `func isa VectorOfVariables` and `index in func.variable` then + * if `length(func.variable) == 1` is one, the constraint is deleted; + * if `length(func.variable) > 1` and `supports_dimension_update(set)` then + then the variable is removed from `func` and `set` is replaced by + `update_dimension(set, MOI.dimension(set) - 1)`. + * Otherwise, a [`DeleteNotAllowed`](@ref) error is thrown. + - Otherwise, the variable is removed from `func`, i.e., it is substituted for + zero. """ delete(model::ModelLike, index::Index) = throw(DeleteNotAllowed(index)) diff --git a/src/sets.jl b/src/sets.jl index b785106ed8..61e2b73681 100644 --- a/src/sets.jl +++ b/src/sets.jl @@ -591,3 +591,35 @@ function Base.copy(set::Union{Reals, Zeros, Nonnegatives, Nonpositives, return set end Base.copy(set::S) where {S <: Union{SOS1, SOS2}} = S(copy(set.weights)) + +""" + supports_dimension_update(S::Type{<:MOI.AbstractVectorSet}) + +Return a `Bool` indicating whether the elimination of any dimension of +`n`-dimensional sets of type `S` give an `n-1`-dimensional set `S`. +By default, this function returns `false` so it should only be implemented +for sets that supports dimension update. + +For instance, `supports_dimension_update(MOI.Nonnegatives}` is `true` because +the elimination of any dimension of the `n`-dimensional nonnegative orthant +gives the `n-1`-dimensional nonnegative orthant. However +`supports_dimension_update(MOI.ExponentialCone}` is `false`. +""" +function supports_dimension_update(::Type{<:AbstractVectorSet}) + return false +end +function supports_dimension_update(::Type{<:Union{ + Reals, Zeros, Nonnegatives, Nonpositives}}) + return true +end + +""" + update_dimension(s::AbstractVectorSet, new_dim) + +Returns a set with the dimension modified to `new_dim`. +""" +function update_dimension end +function update_dimension(set::Union{ + Reals, Zeros, Nonnegatives, Nonpositives}, new_dim) + return typeof(set)(new_dim) +end diff --git a/test/Utilities/functions.jl b/test/Utilities/functions.jl index b83e3040f0..e8690850e9 100644 --- a/test/Utilities/functions.jl +++ b/test/Utilities/functions.jl @@ -75,35 +75,35 @@ end @test MOI.VectorQuadraticTerm(Int32(3), scalarquad) === MOI.VectorQuadraticTerm(Int64(3), scalarquad) @test MOI.VectorQuadraticTerm{Float64}(Int32(3), scalarquad) === MOI.VectorQuadraticTerm(Int64(3), scalarquad) end -@testset "evalvariables" begin +@testset "eval_variables" begin # We do tests twice to make sure the function is not modified vals = Dict(w=>0, x=>3, y=>1, z=>5) fsv = MOI.SingleVariable(z) @test MOI.output_dimension(fsv) == 1 - @test MOIU.evalvariables(vi -> vals[vi], fsv) ≈ 5 - @test MOIU.evalvariables(vi -> vals[vi], fsv) ≈ 5 + @test MOIU.eval_variables(vi -> vals[vi], fsv) ≈ 5 + @test MOIU.eval_variables(vi -> vals[vi], fsv) ≈ 5 fvv = MOI.VectorOfVariables([x, z, y]) @test MOI.output_dimension(fvv) == 3 - @test MOIU.evalvariables(vi -> vals[vi], fvv) ≈ [3, 5, 1] - @test MOIU.evalvariables(vi -> vals[vi], fvv) ≈ [3, 5, 1] + @test MOIU.eval_variables(vi -> vals[vi], fvv) ≈ [3, 5, 1] + @test MOIU.eval_variables(vi -> vals[vi], fvv) ≈ [3, 5, 1] fsa = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x), MOI.ScalarAffineTerm(3.0, z), MOI.ScalarAffineTerm(2.0, y)], 2.0) @test MOI.output_dimension(fsa) == 1 - @test MOIU.evalvariables(vi -> vals[vi], fsa) ≈ 22 - @test MOIU.evalvariables(vi -> vals[vi], fsa) ≈ 22 + @test MOIU.eval_variables(vi -> vals[vi], fsa) ≈ 22 + @test MOIU.eval_variables(vi -> vals[vi], fsa) ≈ 22 fva = MOI.VectorAffineFunction(MOI.VectorAffineTerm.([2, 1, 2], MOI.ScalarAffineTerm.([1.0, 3.0, 2.0], [x, z, y])), [-3.0, 2.0]) @test MOI.output_dimension(fva) == 2 - @test MOIU.evalvariables(vi -> vals[vi], fva) ≈ [12, 7] - @test MOIU.evalvariables(vi -> vals[vi], fva) ≈ [12, 7] + @test MOIU.eval_variables(vi -> vals[vi], fva) ≈ [12, 7] + @test MOIU.eval_variables(vi -> vals[vi], fva) ≈ [12, 7] fsq = MOI.ScalarQuadraticFunction(MOI.ScalarAffineTerm.(1.0, [x, y]), MOI.ScalarQuadraticTerm.(1.0, [x, w, w], [z, z, y]), -3.0) @test MOI.output_dimension(fsq) == 1 - @test MOIU.evalvariables(vi -> vals[vi], fsq) ≈ 16 - @test MOIU.evalvariables(vi -> vals[vi], fsq) ≈ 16 + @test MOIU.eval_variables(vi -> vals[vi], fsq) ≈ 16 + @test MOIU.eval_variables(vi -> vals[vi], fsq) ≈ 16 fvq = MOI.VectorQuadraticFunction(MOI.VectorAffineTerm.([2, 1], MOI.ScalarAffineTerm.(1.0, [x, y])), MOI.VectorQuadraticTerm.([1, 2, 2], MOI.ScalarQuadraticTerm.(1.0, [x, w, w], [z, z, y])), [-3.0, -2.0]) @test MOI.output_dimension(fvq) == 2 - @test MOIU.evalvariables(vi -> vals[vi], fvq) ≈ [13, 1] - @test MOIU.evalvariables(vi -> vals[vi], fvq) ≈ [13, 1] + @test MOIU.eval_variables(vi -> vals[vi], fvq) ≈ [13, 1] + @test MOIU.eval_variables(vi -> vals[vi], fvq) ≈ [13, 1] end @testset "mapvariables" begin fsq = MOI.ScalarQuadraticFunction(MOI.ScalarAffineTerm.(1.0, [x, y]), @@ -285,15 +285,15 @@ end @test f ≈ MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([3, 4], [x, y]), 5) - MOI.SingleVariable(x) end @testset "modification" begin - f = MOIU.modifyfunction(f, MOI.ScalarConstantChange(6)) + f = MOIU.modify_function(f, MOI.ScalarConstantChange(6)) @test f.constant == 6 g = deepcopy(f) @test g ≈ f - f = MOIU.modifyfunction(f, MOI.ScalarCoefficientChange(y, 3)) + f = MOIU.modify_function(f, MOI.ScalarCoefficientChange(y, 3)) @test !(g ≈ f) @test g.terms == MOI.ScalarAffineTerm.([2, 4], [x, y]) @test f.terms == MOI.ScalarAffineTerm.([2, 3], [x, y]) - f = MOIU.modifyfunction(f, MOI.ScalarCoefficientChange(x, 0)) + f = MOIU.modify_function(f, MOI.ScalarCoefficientChange(x, 0)) @test f.terms == MOI.ScalarAffineTerm.([3], [y]) end end @@ -409,13 +409,13 @@ end MOI.ScalarQuadraticTerm.([1.0, 2.0, 3.0], [x, y, x], [x, y, y]), 7.0) / 2.0 end @testset "modification" begin - f = MOIU.modifyfunction(f, MOI.ScalarConstantChange(9)) + f = MOIU.modify_function(f, MOI.ScalarConstantChange(9)) @test f.constant == 9 - f = MOIU.modifyfunction(f, MOI.ScalarCoefficientChange(y, 0)) + f = MOIU.modify_function(f, MOI.ScalarCoefficientChange(y, 0)) @test f.affine_terms == MOI.ScalarAffineTerm.([3], [x]) g = deepcopy(f) @test f ≈ g - f = MOIU.modifyfunction(f, MOI.ScalarCoefficientChange(y, 2)) + f = MOIU.modify_function(f, MOI.ScalarCoefficientChange(y, 2)) @test !(f ≈ g) @test g.affine_terms == MOI.ScalarAffineTerm.([3], [x]) @test f.affine_terms == MOI.ScalarAffineTerm.([3, 2], [x, y]) @@ -430,26 +430,26 @@ end @test MOI.output_dimension(f) == 2 @test f.terms == MOI.VectorAffineTerm.([1, 1, 2], MOI.ScalarAffineTerm.([2, 4, 3], [x, y, y])) @test MOIU.constant_vector(f) == [5, 7] - f = MOIU.modifyfunction(f, MOI.VectorConstantChange([6, 8])) + f = MOIU.modify_function(f, MOI.VectorConstantChange([6, 8])) @test MOIU.constant_vector(f) == [6, 8] g = deepcopy(f) @test f ≈ g - f = MOIU.modifyfunction(f, MOI.MultirowChange(y, [(2, 9)])) + f = MOIU.modify_function(f, MOI.MultirowChange(y, [(2, 9)])) @test !(f ≈ g) @test f.terms == MOI.VectorAffineTerm.([1, 1, 2], MOI.ScalarAffineTerm.([2, 4, 9], [x, y, y])) @test g.terms == MOI.VectorAffineTerm.([1, 1, 2], MOI.ScalarAffineTerm.([2, 4, 3], [x, y, y])) - f = MOIU.modifyfunction(f, MOI.MultirowChange(y, [(1, 0)])) + f = MOIU.modify_function(f, MOI.MultirowChange(y, [(1, 0)])) @test f.terms == MOI.VectorAffineTerm.([1, 2], MOI.ScalarAffineTerm.([2, 9], [x, y])) end @testset "Quadratic" begin f = MOI.VectorQuadraticFunction(MOI.VectorAffineTerm.([1, 2, 2], MOI.ScalarAffineTerm.([3, 1, 2], [x, x, y])), MOI.VectorQuadraticTerm.([1, 1, 2], MOI.ScalarQuadraticTerm.([1, 2, 3], [x, y, x], [x, y, y])), [7, 3, 4]) @test MOI.output_dimension(f) == 3 - f = MOIU.modifyfunction(f, MOI.VectorConstantChange([10, 11, 12])) + f = MOIU.modify_function(f, MOI.VectorConstantChange([10, 11, 12])) @test MOIU.constant_vector(f) == [10, 11, 12] - f = MOIU.modifyfunction(f, MOI.MultirowChange(y, [(2, 0), (1, 1)])) + f = MOIU.modify_function(f, MOI.MultirowChange(y, [(2, 0), (1, 1)])) @test f.affine_terms == MOI.VectorAffineTerm.([1, 2, 1], MOI.ScalarAffineTerm.([3, 1, 1], [x, x, y])) g = deepcopy(f) - f = MOIU.modifyfunction(f, MOI.MultirowChange(x, [(1, 0), (3, 4)])) + f = MOIU.modify_function(f, MOI.MultirowChange(x, [(1, 0), (3, 4)])) @test f.affine_terms == MOI.VectorAffineTerm.([2, 1, 3], MOI.ScalarAffineTerm.([1, 1, 4], [x, y, x])) @test g.affine_terms == MOI.VectorAffineTerm.([1, 2, 1], MOI.ScalarAffineTerm.([3, 1, 1], [x, x, y])) end @@ -461,20 +461,20 @@ end (MOI.coefficient.(f1.terms) ≈ MOI.coefficient.(f2.terms))) end function test_canonicalization(f::T, expected::T) where {T <: Union{MOI.ScalarAffineFunction, MOI.VectorAffineFunction}} - @test MOIU.iscanonical(expected) + @test MOIU.is_canonical(expected) g = @inferred(MOIU.canonical(f)) @test isapprox_ordered(g, expected) - @test MOIU.iscanonical(g) - @test MOIU.iscanonical(expected) + @test MOIU.is_canonical(g) + @test MOIU.is_canonical(expected) @test isapprox_ordered(MOIU.canonical(g), g) @test MOIU.canonical(g) !== g @test @allocated(MOIU.canonicalize!(f)) == 0 end @testset "ScalarAffine" begin - @test MOIU.iscanonical(MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(Float64[], MOI.VariableIndex.(Int[])), 1.0)) - @test !MOIU.iscanonical(MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([0.0], MOI.VariableIndex.([1])), 1.0)) - @test !MOIU.iscanonical(MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0, 2.0], MOI.VariableIndex.([2, 1])), 2.0)) - @test !MOIU.iscanonical(MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0, 0.0], MOI.VariableIndex.([1, 2])), 2.0)) + @test MOIU.is_canonical(MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(Float64[], MOI.VariableIndex.(Int[])), 1.0)) + @test !MOIU.is_canonical(MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([0.0], MOI.VariableIndex.([1])), 1.0)) + @test !MOIU.is_canonical(MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0, 2.0], MOI.VariableIndex.([2, 1])), 2.0)) + @test !MOIU.is_canonical(MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0, 0.0], MOI.VariableIndex.([1, 2])), 2.0)) test_canonicalization( MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(Float64[], MOI.VariableIndex[]), 1.5), @@ -526,12 +526,12 @@ end ) end @testset "VectorAffine" begin - @test MOIU.iscanonical(MOI.VectorAffineFunction(MOI.VectorAffineTerm.(Int[], MOI.ScalarAffineTerm.(Float64[], MOI.VariableIndex.(Int[]))), Float64[])) - @test !MOIU.iscanonical(MOI.VectorAffineFunction(MOI.VectorAffineTerm.([1], MOI.ScalarAffineTerm.([0.0], MOI.VariableIndex.([1]))), [1.0])) - @test !MOIU.iscanonical(MOI.VectorAffineFunction(MOI.VectorAffineTerm.([1, 2], MOI.ScalarAffineTerm.([0.0, 1.0], MOI.VariableIndex.([1, 2]))), [1.0])) - @test !MOIU.iscanonical(MOI.VectorAffineFunction(MOI.VectorAffineTerm.([2, 1], MOI.ScalarAffineTerm.([1.0, 1.0], MOI.VariableIndex.([1, 2]))), [1.0])) - @test !MOIU.iscanonical(MOI.VectorAffineFunction(MOI.VectorAffineTerm.([1, 1], MOI.ScalarAffineTerm.([1.0, -1.0], MOI.VariableIndex.([1, 1]))), [1.0])) - @test !MOIU.iscanonical(MOI.VectorAffineFunction(MOI.VectorAffineTerm.([1, 1], MOI.ScalarAffineTerm.([1.0, 1.0], MOI.VariableIndex.([1, 1]))), [1.0])) + @test MOIU.is_canonical(MOI.VectorAffineFunction(MOI.VectorAffineTerm.(Int[], MOI.ScalarAffineTerm.(Float64[], MOI.VariableIndex.(Int[]))), Float64[])) + @test !MOIU.is_canonical(MOI.VectorAffineFunction(MOI.VectorAffineTerm.([1], MOI.ScalarAffineTerm.([0.0], MOI.VariableIndex.([1]))), [1.0])) + @test !MOIU.is_canonical(MOI.VectorAffineFunction(MOI.VectorAffineTerm.([1, 2], MOI.ScalarAffineTerm.([0.0, 1.0], MOI.VariableIndex.([1, 2]))), [1.0])) + @test !MOIU.is_canonical(MOI.VectorAffineFunction(MOI.VectorAffineTerm.([2, 1], MOI.ScalarAffineTerm.([1.0, 1.0], MOI.VariableIndex.([1, 2]))), [1.0])) + @test !MOIU.is_canonical(MOI.VectorAffineFunction(MOI.VectorAffineTerm.([1, 1], MOI.ScalarAffineTerm.([1.0, -1.0], MOI.VariableIndex.([1, 1]))), [1.0])) + @test !MOIU.is_canonical(MOI.VectorAffineFunction(MOI.VectorAffineTerm.([1, 1], MOI.ScalarAffineTerm.([1.0, 1.0], MOI.VariableIndex.([1, 1]))), [1.0])) test_canonicalization( MOI.VectorAffineFunction(MOI.VectorAffineTerm.(Int[], MOI.ScalarAffineTerm.(Float64[], MOI.VariableIndex.(Int[]))), Float64[]), diff --git a/test/Utilities/mockoptimizer.jl b/test/Utilities/mockoptimizer.jl index ef9f6b78d2..204deef88f 100644 --- a/test/Utilities/mockoptimizer.jl +++ b/test/Utilities/mockoptimizer.jl @@ -90,3 +90,8 @@ end @test MOI.get(optimizer, MOI.ConstraintDual(), c1) == 5.9 @test MOI.get(optimizer, MOI.ConstraintDual(), soc) == [1.0,2.0] end + +@testset "Delete" begin + mock = MOIU.MockOptimizer(MOIU.Model{Float64}()) + MOIT.delete_test(mock) +end diff --git a/test/Utilities/model.jl b/test/Utilities/model.jl index b27e0dedeb..cb56f5e0e5 100644 --- a/test/Utilities/model.jl +++ b/test/Utilities/model.jl @@ -59,6 +59,10 @@ end MOIT.validtest(MOIU.Model{Float64}()) end +@testset "Delete test" begin + MOIT.delete_test(MOIU.Model{Float64}()) +end + @testset "Empty test" begin MOIT.emptytest(MOIU.Model{Float64}()) end @@ -107,8 +111,8 @@ end @test (MOI.VectorQuadraticFunction{Int},MOI.PositiveSemidefiniteConeTriangle) in loc @test (MOI.VectorQuadraticFunction{Int},MOI.PositiveSemidefiniteConeTriangle) in loc - f3 = MOIU.modifyfunction(f1, MOI.ScalarConstantChange(9)) - f3 = MOIU.modifyfunction(f3, MOI.ScalarCoefficientChange(y, 2)) + f3 = MOIU.modify_function(f1, MOI.ScalarConstantChange(9)) + f3 = MOIU.modify_function(f3, MOI.ScalarCoefficientChange(y, 2)) @test !(MOI.get(model, MOI.ConstraintFunction(), c1) ≈ f3) MOI.set(model, MOI.ConstraintFunction(), c1, f3) @@ -130,6 +134,10 @@ end c7 = MOI.add_constraint(model, f7, MOI.Nonpositives(2)) @test 1 == @inferred MOI.get(model, MOI.NumberOfConstraints{MOI.VectorOfVariables,MOI.Nonpositives}()) + f8 = MOI.VectorOfVariables([x, y]) + c8 = MOI.add_constraint(model, f8, MOI.SecondOrderCone(2)) + @test 1 == @inferred MOI.get(model, MOI.NumberOfConstraints{MOI.VectorOfVariables,MOI.SecondOrderCone}()) + loc1 = MOI.get(model, MOI.ListOfConstraints()) loc2 = Vector{Tuple{DataType, DataType}}() function _pushloc(constrs::Vector{MOIU.ConstraintEntry{F, S}}) where {F, S} @@ -139,7 +147,7 @@ end end MOIU.broadcastcall(_pushloc, model) for loc in (loc1, loc2) - @test length(loc) == 5 + @test length(loc) == 6 @test (MOI.VectorQuadraticFunction{Int},MOI.PositiveSemidefiniteConeTriangle) in loc @test (MOI.VectorQuadraticFunction{Int},MOI.PositiveSemidefiniteConeTriangle) in loc @test (MOI.VectorOfVariables,MOI.RotatedSecondOrderCone) in loc @@ -154,6 +162,16 @@ end @test 1 == @inferred MOI.get(model, MOI.NumberOfConstraints{MOI.VectorAffineFunction{Int},MOI.SecondOrderCone}()) @test MOI.get(model, MOI.ConstraintFunction(), c6).constants == f6.constants + message = string("Cannot delete variable as it is constrained with other", + " variables in a `MOI.VectorOfVariables`.") + err = MOI.DeleteNotAllowed(y, message) + @test_throws err MOI.delete(model, y) + + @test MOI.is_valid(model, c8) + MOI.delete(model, c8) + @test !MOI.is_valid(model, c8) + @test 0 == @inferred MOI.get(model, MOI.NumberOfConstraints{MOI.VectorOfVariables,MOI.SecondOrderCone}()) + MOI.delete(model, y) f = MOI.get(model, MOI.ConstraintFunction(), c2) @@ -165,12 +183,14 @@ end @test f.terms == MOI.VectorAffineTerm.([1], MOI.ScalarAffineTerm.([2], [x])) @test f.constants == [6, 8] + @test 1 == @inferred MOI.get(model, MOI.NumberOfConstraints{MOI.VectorOfVariables,MOI.Nonpositives}()) + @test [c7] == @inferred MOI.get(model, MOI.ListOfConstraintIndices{MOI.VectorOfVariables,MOI.Nonpositives}()) + f = MOI.get(model, MOI.ConstraintFunction(), c7) @test f.variables == [x] s = MOI.get(model, MOI.ConstraintSet(), c7) @test MOI.dimension(s) == 1 - end # We create a new function and set to test catching errors if users create their diff --git a/test/Utilities/universalfallback.jl b/test/Utilities/universalfallback.jl index 47283486e7..2bf41a9640 100644 --- a/test/Utilities/universalfallback.jl +++ b/test/Utilities/universalfallback.jl @@ -204,3 +204,9 @@ end "a name") @test_throws Exception MOI.get(uf, MOI.ConstraintIndex, "a name") end + +@testset "Delete" begin + model = ModelForUniversalFallback{Float64}() + uf = MOIU.UniversalFallback(model) + MOIT.delete_test(uf) +end