diff --git a/docs/src/manual/basic_usage.md b/docs/src/manual/basic_usage.md index 45d1459e0a..d2d2210237 100644 --- a/docs/src/manual/basic_usage.md +++ b/docs/src/manual/basic_usage.md @@ -291,7 +291,6 @@ MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE) # output -MAX_SENSE::OptimizationSense = 1 ``` We add the knapsack constraint and integrality constraints: diff --git a/src/Utilities/CleverDicts.jl b/src/Utilities/CleverDicts.jl index c9baf9c71f..ba3260a81c 100644 --- a/src/Utilities/CleverDicts.jl +++ b/src/Utilities/CleverDicts.jl @@ -10,7 +10,11 @@ function index_to_key(::Type{MathOptInterface.VariableIndex}, index::Int64) return MathOptInterface.VariableIndex(index) end -key_to_index(key::MathOptInterface.VariableIndex) = key.value +function index_to_key(::Type{MathOptInterface.ConstraintIndex{F,S}}, index::Int64) where {F,S} + return MathOptInterface.ConstraintIndex{F,S}(index) +end + +key_to_index(key::MathOptInterface.Index) = key.value # Now, on with `CleverDicts`. @@ -62,22 +66,6 @@ mutable struct CleverDict{K,V,F<:Function,I<:Function} <: AbstractDict{K,V} set::BitSet vector::Vector{V} dict::OrderedCollections.OrderedDict{K,V} - function CleverDict{K,V}(n::Integer = 0) where {K,V} - set = BitSet() - sizehint!(set, n) - vec = Vector{K}(undef, n) - inverse_hash = x -> index_to_key(K, x) - hash = key_to_index - return new{K,V,typeof(hash),typeof(inverse_hash)}( - 0, - hash, - inverse_hash, - true, - set, - vec, - OrderedCollections.OrderedDict{K,V}(), - ) - end function CleverDict{K,V}( hash::F, inverse_hash::I, @@ -97,6 +85,9 @@ mutable struct CleverDict{K,V,F<:Function,I<:Function} <: AbstractDict{K,V} ) end end +function CleverDict{K,V}(n::Integer = 0) where {K,V} + return CleverDict{K,V}(key_to_index, Base.Fix1(index_to_key, K), n) +end """ index_to_key(::Type{K}, index::Int) @@ -151,6 +142,14 @@ function Base.haskey(c::CleverDict{K}, key::K) where {K} return _is_dense(c) ? c.hash(key)::Int64 in c.set : haskey(c.dict, key) end +function Base.keys(c::CleverDict{K}) where {K} + return if _is_dense(c) + [c.inverse_hash(Int64(index))::K for index in c.set] + else + collect(keys(c.dict)) + end +end + function Base.get(c::CleverDict, key, default) if _is_dense(c) if !haskey(c, key) @@ -363,4 +362,19 @@ function Base.resize!(c::CleverDict{K,V}, n) where {K,V} return end +Base.values(d::CleverDict) = _is_dense(d) ? d.vector : values(d.dict) + +# TODO `map!(f, values(dict::AbstractDict))` requires Julia 1.2 or later, +# use `map_values` once we drop Julia 1.1 and earlier. +function map_values!(f::Function, d::CleverDict) + if _is_dense(d) + map!(f, d.vector, d.vector) + else + for (k, v) in d.dict + d.dict[k] = f(v) + end + end + return +end + end diff --git a/src/Utilities/DoubleDicts.jl b/src/Utilities/DoubleDicts.jl index 48f0c7e4e6..d9f3b5b879 100644 --- a/src/Utilities/DoubleDicts.jl +++ b/src/Utilities/DoubleDicts.jl @@ -19,7 +19,7 @@ Works as a `AbstractDict{CI, V}` with minimal differences. Note that `CI` is not a concrete type, opposed to `CI{MOI.SingleVariable, MOI.Integers}`, which is a concrete type. -When optimal performance or type stability is required its possible to obtain a +When optimal performance or type stability is required it is possible to obtain a fully type stable dictionary with values of type `V` and keys of type `CI{MOI.SingleVariable, MOI.Integers}` from the dictionary `dict`, for instance: diff --git a/src/Utilities/Utilities.jl b/src/Utilities/Utilities.jl index 4868cc2589..d55c3ead86 100644 --- a/src/Utilities/Utilities.jl +++ b/src/Utilities/Utilities.jl @@ -57,6 +57,7 @@ include("copy.jl") include("results.jl") include("variables.jl") +include("vector_of_constraints.jl") include("model.jl") include("parser.jl") include("mockoptimizer.jl") diff --git a/src/Utilities/model.jl b/src/Utilities/model.jl index 040b06750d..861b804c47 100644 --- a/src/Utilities/model.jl +++ b/src/Utilities/model.jl @@ -1,124 +1,19 @@ -## Storage of constraints -# -# All `F`-in-`S` constraints are stored in a vector of `ConstraintEntry{F, S}`. -# The index in this vector of a constraint of index -# `ci::MOI.ConstraintIndex{F, S}` is given by `model.constrmap[ci.value]`. The -# advantage of this representation is that it does not require any dictionary -# hence it never needs to compute a hash. -# -# It may seem redundant to store the constraint index `ci` as well as the -# function and sets in the tuple but it is used to efficiently implement the -# getter for `MOI.ListOfConstraintIndices{F, S}`. It is also used to implement -# `MOI.delete`. Indeed, when a constraint is deleted, it is removed from the -# vector hence the index in the vector of all the functions that were stored -# after must be decreased by one. As the constraint index is stored in the -# vector, it readily gives the entries of `model.constrmap` that need to be -# updated. -const ConstraintEntry{F,S} = Tuple{CI{F,S},F,S} - const EMPTYSTRING = "" -# Implementation of MOI for vector of constraint -function _add_constraint( - constrs::Vector{ConstraintEntry{F,S}}, - ci::CI, - f::F, - s::S, -) where {F,S} - push!(constrs, (ci, f, s)) - return length(constrs) -end - -function _delete(constrs::Vector, ci::CI, i::Int) - deleteat!(constrs, i) - @view constrs[i:end] # will need to shift it in constrmap -end - -_getindex(ci::CI, f::MOI.AbstractFunction, s::MOI.AbstractSet) = ci -function _getindex(constrs::Vector, ci::CI, i::Int) - return _getindex(constrs[i]...) -end - -_getfun(ci::CI, f::MOI.AbstractFunction, s::MOI.AbstractSet) = f -function _getfunction(constrs::Vector, ci::CI, i::Int) - if !(1 ≤ i ≤ length(constrs)) - throw(MOI.InvalidIndex(ci)) - end - @assert ci.value == constrs[i][1].value - return _getfun(constrs[i]...) -end - -_gets(ci::CI, f::MOI.AbstractFunction, s::MOI.AbstractSet) = s -function _getset(constrs::Vector, ci::CI, i::Int) - if !(1 ≤ i ≤ length(constrs)) - throw(MOI.InvalidIndex(ci)) - end - @assert ci.value == constrs[i][1].value - return _gets(constrs[i]...) -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) -function _modifyconstr( - ci::CI{F,S}, - f::F, - s::S, - change::MOI.AbstractFunctionModification, -) where {F,S} - return (ci, modify_function(f, change), s) -end -function _modify( - constrs::Vector{ConstraintEntry{F,S}}, - ci::CI{F}, - i::Int, - change, -) where {F,S} - return constrs[i] = _modifyconstr(constrs[i]..., change) -end - -function _getnoc( - constrs::Vector{ConstraintEntry{F,S}}, - ::MOI.NumberOfConstraints{F,S}, -) where {F,S} - return length(constrs) -end -# Might be called when calling NumberOfConstraint with different coefficient type than the one supported -_getnoc(::Vector, ::MOI.NumberOfConstraints) = 0 - -function _getloc( - constrs::Vector{ConstraintEntry{F,S}}, -)::Vector{Tuple{DataType,DataType}} where {F,S} - return isempty(constrs) ? [] : [(F, S)] -end - -function _getlocr( - constrs::Vector{ConstraintEntry{F,S}}, - ::MOI.ListOfConstraintIndices{F,S}, -) where {F,S} - return map(constr -> constr[1], constrs) -end -function _getlocr( - constrs::Vector{<:ConstraintEntry}, - ::MOI.ListOfConstraintIndices{F,S}, -) where {F,S} - return CI{F,S}[] -end - # Implementation of MOI for AbstractModel abstract type AbstractModelLike{T} <: MOI.ModelLike end abstract type AbstractOptimizer{T} <: MOI.AbstractOptimizer end const AbstractModel{T} = Union{AbstractModelLike{T},AbstractOptimizer{T}} -getconstrloc(model::AbstractModel, ci::CI) = model.constrmap[ci.value] - # Variables function MOI.get(model::AbstractModel, ::MOI.NumberOfVariables)::Int64 if model.variable_indices === nothing - model.num_variables_created + return model.num_variables_created else - length(model.variable_indices) + return length(model.variable_indices) end end + function MOI.add_variable(model::AbstractModel{T}) where {T} vi = VI(model.num_variables_created += 1) push!(model.single_variable_mask, 0x0) @@ -129,6 +24,7 @@ function MOI.add_variable(model::AbstractModel{T}) where {T} end return vi end + function MOI.add_variables(model::AbstractModel, n::Integer) return [MOI.add_variable(model) for i in 1:n] end @@ -152,22 +48,9 @@ function remove_variable(f::MOI.VectorOfVariables, s, vi::VI) end return g, t end -function _remove_variable(constrs::Vector{<:ConstraintEntry}, vi::VI) - for i in eachindex(constrs) - ci, f, s = constrs[i] - constrs[i] = (ci, remove_variable(f, s, vi)...) - end -end -function filter_variables(keep::F, f, s) where {F<:Function} - return filter_variables(keep, f), s -end - -function filter_variables( - keep::F, - f::MOI.VectorOfVariables, - s, -) where {F<:Function} +filter_variables(keep::F, f, s) where {F<:Function} = filter_variables(keep, f), s +function filter_variables(keep::F, f::MOI.VectorOfVariables, s) where {F<:Function} g = filter_variables(keep, f) if length(g.variables) != length(f.variables) t = MOI.update_dimension(s, length(g.variables)) @@ -176,71 +59,11 @@ function filter_variables( end return g, t end - -function _filter_variables( - keep::F, - constrs::Vector{<:ConstraintEntry}, -) where {F<:Function} - for i in eachindex(constrs) - ci, f, s = constrs[i] - constrs[i] = (ci, filter_variables(keep, f, s)...) - end -end -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`.", - ) - return 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 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 _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 - return rm -end function _delete_variable( model::AbstractModel{T}, vi::MOI.VariableIndex, ) where {T} MOI.throw_if_not_valid(model, 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 model.single_variable_mask[vi.value] = 0x0 if model.variable_indices === nothing model.variable_indices = @@ -284,33 +107,43 @@ function _delete_variable( ) end function MOI.delete(model::AbstractModel, vi::MOI.VariableIndex) + vis = [vi] + broadcastcall(model) do constrs + _throw_if_cannot_delete(constrs, vis, vis) + end _delete_variable(model, vi) - # `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) - return model.objective = remove_variable(model.objective, vi) + broadcastcall(model) do constrs + _deleted_constraints(constrs, vi) do ci + delete!(model.con_to_name, ci) + end + end + model.objective = remove_variable(model.objective, vi) + model.name_to_con = nothing + return end + function MOI.delete(model::AbstractModel, vis::Vector{MOI.VariableIndex}) if isempty(vis) # In `keep`, we assume that `model.variable_indices !== nothing` so # at least one variable need to be deleted. return end - # 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) + fast_in_vis = Set(vis) + broadcastcall(model) do constrs + _throw_if_cannot_delete(constrs, vis, fast_in_vis) + end + broadcastcall(model) do constrs + _deleted_constraints(constrs, vis) do ci + delete!(model.con_to_name, ci) + end end for vi in vis _delete_variable(model, vi) end - # `VectorOfVariables` constraints with sets not supporting dimension update - # were either deleted or an error was thrown. The rest is modified now. keep(vi::MOI.VariableIndex) = vi in model.variable_indices model.objective = filter_variables(keep, model.objective) - return broadcastcall(constrs -> _filter_variables(keep, constrs), model) + model.name_to_con = nothing + return end function MOI.is_valid( @@ -322,20 +155,15 @@ function MOI.is_valid( model.single_variable_mask[ci.value] & single_variable_flag(S), ) end + function MOI.is_valid(model::AbstractModel, ci::CI{F,S}) where {F,S} - if ci.value > length(model.constrmap) - false + if MOI.supports_constraint(model, F, S) + return MOI.is_valid(constraints(model, ci), ci) else - loc = getconstrloc(model, ci) - if iszero(loc) # This means that it has been deleted - false - elseif loc > MOI.get(model, MOI.NumberOfConstraints{F,S}()) - false - else - ci == _getindex(model, ci, getconstrloc(model, ci)) - end + return false end end + function MOI.is_valid(model::AbstractModel, vi::VI) if model.variable_indices === nothing return 1 ≤ vi.value ≤ model.num_variables_created @@ -364,8 +192,10 @@ MOI.get(model::AbstractModel, ::MOI.Name) = model.name MOI.supports(::AbstractModel, ::MOI.VariableName, vi::Type{VI}) = true function MOI.set(model::AbstractModel, ::MOI.VariableName, vi::VI, name::String) model.var_to_name[vi] = name - return model.name_to_var = nothing # Invalidate the name map. + model.name_to_var = nothing # Invalidate the name map. + return end + function MOI.get(model::AbstractModel, ::MOI.VariableName, vi::VI) return get(model.var_to_name, vi, EMPTYSTRING) end @@ -429,8 +259,10 @@ function MOI.set( name::String, ) model.con_to_name[ci] = name - return model.name_to_con = nothing # Invalidate the name map. + model.name_to_con = nothing # Invalidate the name map. + return end + function MOI.get(model::AbstractModel, ::MOI.ConstraintName, ci::CI) return get(model.con_to_name, ci, EMPTYSTRING) end @@ -462,11 +294,7 @@ function MOI.get(model::AbstractModel, ConType::Type{<:CI}, name::String) end ci = get(model.name_to_con, name, nothing) throw_if_multiple_with_name(ci, name) - if ci isa ConType - return ci - else - return nothing - end + return ci isa ConType ? ci : nothing end function MOI.get( @@ -489,8 +317,10 @@ function MOI.set( model.objective = zero(MOI.ScalarAffineFunction{T}) end model.senseset = true - return model.sense = sense + model.sense = sense + return end + function MOI.get(model::AbstractModel, ::MOI.ObjectiveFunctionType) return MOI.typeof(model.objective) end @@ -517,7 +347,8 @@ function MOI.set( end model.objectiveset = true # f needs to be copied, see #2 - return model.objective = copy(f) + model.objective = copy(f) + return end function MOI.modify( @@ -678,98 +509,91 @@ function MOI.add_constraint( return CI{MOI.SingleVariable,typeof(s)}(index) end -function MOI.add_constraint( - model::AbstractModel, - f::F, - s::S, -) where {F<:MOI.AbstractFunction,S<:MOI.AbstractSet} - if MOI.supports_constraint(model, F, S) - # We give the index value `nextconstraintid + 1` to the new constraint. - # As the same counter is used for all pairs of F-in-S constraints, - # the index value is unique across all constraint types as mentioned in - # `@model`'s doc. - ci = CI{F,S}(model.nextconstraintid += 1) - # f needs to be copied, see #2 - # We canonicalize the constraint so that solvers can avoid having to canonicalize - # it most of the time (they can check if they need to with `is_canonical`. - # Note that the canonicalization is not guaranteed if for instance - # `modify` is called and adds a new term. - # See https://github.com/jump-dev/MathOptInterface.jl/pull/1118 - push!( - model.constrmap, - _add_constraint(model, ci, canonical(f), copy(s)), - ) - return ci +function MOI.add_constraint(model::AbstractModel, func::MOI.AbstractFunction, set::MOI.AbstractSet) + if MOI.supports_constraint(model, typeof(func), typeof(set)) + return MOI.add_constraint(constraints(model, typeof(func), typeof(set)), func, set) else - throw(MOI.UnsupportedConstraint{F,S}()) + throw(MOI.UnsupportedConstraint{typeof(func),typeof(set)}()) end end +function constraints( + model::AbstractModel, + ci::MOI.ConstraintIndex{F,S} +) where {F,S} + if !MOI.supports_constraint(model, F, S) + throw(MOI.InvalidIndex(ci)) + end + return constraints(model, F, S) +end +function MOI.get(model::AbstractModel, attr::Union{MOI.AbstractFunction, MOI.AbstractSet}, ci::MOI.ConstraintIndex) + return MOI.get(constraints(model, ci), attr, ci) +end +function MOI.modify(model::AbstractModel, ci::MOI.ConstraintIndex, change) + return MOI.modify(constraints(model, ci), ci, change) +end function _delete_constraint( model::AbstractModel, - ci::CI{MOI.SingleVariable,S}, + ci::MOI.ConstraintIndex{MOI.SingleVariable,S}, ) where {S} - return model.single_variable_mask[ci.value] &= ~single_variable_flag(S) + MOI.throw_if_not_valid(model, ci) + model.single_variable_mask[ci.value] &= ~single_variable_flag(S) + return end -function _delete_constraint(model::AbstractModel, ci::CI) - for (ci_next, _, _) in _delete(model, ci, getconstrloc(model, ci)) - model.constrmap[ci_next.value] -= 1 - end - return model.constrmap[ci.value] = 0 + +function _delete_constraint(model::AbstractModel, ci::MOI.ConstraintIndex) + MOI.delete(constraints(model, ci), ci) + return end -function MOI.delete(model::AbstractModel, ci::CI) - MOI.throw_if_not_valid(model, ci) + +function MOI.delete(model::AbstractModel, ci::MOI.ConstraintIndex) _delete_constraint(model, ci) model.name_to_con = nothing - return delete!(model.con_to_name, ci) + delete!(model.con_to_name, ci) + return end function MOI.modify( model::AbstractModel, - ci::CI, + ci::MOI.ConstraintIndex, change::MOI.AbstractFunctionModification, ) - return _modify(model, ci, getconstrloc(model, ci), change) + MOI.modify(constraints(model, ci), ci, change) + return end function MOI.set( - model::AbstractModel, + ::AbstractModel, ::MOI.ConstraintFunction, - ci::CI{MOI.SingleVariable}, - change::MOI.AbstractFunction, + ::CI{MOI.SingleVariable}, + ::MOI.SingleVariable, ) return throw(MOI.SettingSingleVariableFunctionNotAllowed()) end function MOI.set( - model::AbstractModel, - ::MOI.ConstraintFunction, - ci::CI, - change::MOI.AbstractFunction, -) - return _modify(model, ci, getconstrloc(model, ci), change) -end -function MOI.set( - model::AbstractModel, + model::AbstractModel{T}, ::MOI.ConstraintSet, ci::CI{MOI.SingleVariable}, - change::MOI.AbstractSet, -) + set::SUPPORTED_VARIABLE_SCALAR_SETS{T}, +) where {T} MOI.throw_if_not_valid(model, ci) - flag = single_variable_flag(typeof(change)) + flag = single_variable_flag(typeof(set)) if !iszero(flag & LOWER_BOUND_MASK) - model.lower_bound[ci.value] = extract_lower_bound(change) + model.lower_bound[ci.value] = extract_lower_bound(set) end if !iszero(flag & UPPER_BOUND_MASK) - model.upper_bound[ci.value] = extract_upper_bound(change) + model.upper_bound[ci.value] = extract_upper_bound(set) end + return end function MOI.set( model::AbstractModel, - ::MOI.ConstraintSet, - ci::CI, - change::MOI.AbstractSet, + attr::Union{MOI.ConstraintFunction, MOI.ConstraintSet}, + ci::MOI.ConstraintIndex, + func_or_set, ) - return _modify(model, ci, getconstrloc(model, ci), change) + MOI.set(constraints(model, ci), attr, ci, func_or_set) + return end function MOI.get( @@ -779,8 +603,12 @@ function MOI.get( flag = single_variable_flag(S) return count(mask -> !iszero(flag & mask), model.single_variable_mask) end -function MOI.get(model::AbstractModel, noc::MOI.NumberOfConstraints) - return _getnoc(model, noc) +function MOI.get(model::AbstractModel, noc::MOI.NumberOfConstraints{F,S}) where {F,S} + if MOI.supports_constraint(model, F, S) + return MOI.get(constraints(model, F, S), noc) + else + return 0 + end end function _add_contraint_type( @@ -795,7 +623,9 @@ function _add_contraint_type( return end function MOI.get(model::AbstractModel{T}, loc::MOI.ListOfConstraints) where {T} - list = broadcastvcat(_getloc, model) + list = broadcastvcat(model) do v + MOI.get(v, loc) + end for S in ( MOI.EqualTo{T}, MOI.GreaterThan{T}, @@ -824,8 +654,13 @@ function MOI.get( end return list end -function MOI.get(model::AbstractModel, loc::MOI.ListOfConstraintIndices) - return broadcastvcat(constrs -> _getlocr(constrs, loc), model) + +function MOI.get(model::AbstractModel, loc::MOI.ListOfConstraintIndices{F,S}) where {F,S} + if MOI.supports_constraint(model, F, S) + return MOI.get(constraints(model, F, S), loc) + else + return MOI.ConstraintIndex{F,S}[] + end end function MOI.get( @@ -836,8 +671,12 @@ function MOI.get( MOI.throw_if_not_valid(model, ci) return MOI.SingleVariable(MOI.VariableIndex(ci.value)) end -function MOI.get(model::AbstractModel, ::MOI.ConstraintFunction, ci::CI) - return _getfunction(model, ci, getconstrloc(model, ci)) +function MOI.get( + model::AbstractModel, + attr::Union{MOI.ConstraintFunction, MOI.ConstraintSet}, + ci::MOI.ConstraintIndex +) + return MOI.get(constraints(model, ci), attr, ci) end function _get_single_variable_set( @@ -870,7 +709,7 @@ function _get_single_variable_set( return S(model.lower_bound[index], model.upper_bound[index]) end function _get_single_variable_set( - model::AbstractModel, + ::AbstractModel, S::Type{<:Union{MOI.Integer,MOI.ZeroOne}}, index, ) @@ -884,9 +723,6 @@ function MOI.get( MOI.throw_if_not_valid(model, ci) return _get_single_variable_set(model, S, ci.value) end -function MOI.get(model::AbstractModel, ::MOI.ConstraintSet, ci::CI) - return _getset(model, ci, getconstrloc(model, ci)) -end function MOI.is_empty(model::AbstractModel) return isempty(model.name) && @@ -895,9 +731,28 @@ function MOI.is_empty(model::AbstractModel) isempty(model.objective.terms) && iszero(model.objective.constant) && iszero(model.num_variables_created) && - iszero(model.nextconstraintid) + mapreduce_constraints(MOI.is_empty, &, model, true) +end +function MOI.empty!(model::AbstractModel{T}) where {T} + model.name = "" + model.senseset = false + model.sense = MOI.FEASIBILITY_SENSE + model.objectiveset = false + model.objective = zero(MOI.ScalarAffineFunction{T}) + model.num_variables_created = 0 + model.variable_indices = nothing + model.single_variable_mask = UInt8[] + model.lower_bound = T[] + model.upper_bound = T[] + empty!(model.var_to_name) + model.name_to_var = nothing + empty!(model.con_to_name) + model.name_to_con = nothing + broadcastcall(MOI.empty!, model) + return end + function MOI.copy_to(dest::AbstractModel, src::MOI.ModelLike; kws...) return automatic_copy_to(dest, src; kws...) end @@ -967,6 +822,8 @@ MOIU.broadcastvcat(_getfuns, model) """ function broadcastvcat end +function mapreduce_constraints end + # Macro to generate Model abstract type Constraints{F} end @@ -1011,14 +868,16 @@ using Unicode _field(s::SymbolFS) = Symbol(replace(lowercase(string(s.s)), "." => "_")) -_getC(s::SymbolSet) = :(ConstraintEntry{F,$(_typedset(s))}) +_getC(s::SymbolSet) = :(VectorOfConstraints{F,$(_typedset(s))}) _getC(s::SymbolFun) = _typedfun(s) -_getCV(s::SymbolSet) = :($(_getC(s))[]) +_getCV(s::SymbolSet) = :($(_getC(s))()) _getCV(s::SymbolFun) = :($(s.cname){T,$(_getC(s))}()) _callfield(f, s::SymbolFS) = :($f(model.$(_field(s)))) _broadcastfield(b, s::SymbolFS) = :($b(f, model.$(_field(s)))) +_mapreduce_field(s::SymbolFS) = :(cur = $MOIU.mapreduce_constraints(f, op, model.$(_field(s)), cur)) +_mapreduce_constraints(s::SymbolFS) = :(cur = op(cur, f(model.$(_field(s))))) # This macro is for expert/internal use only. Prefer the concrete Model type # instantiated below. @@ -1124,10 +983,8 @@ mutable struct LPModel{T} <: MOIU.AbstractModel{T} var_to_name::Dict{MOI.VariableIndex, String} # If `nothing`, the dictionary hasn't been constructed yet. name_to_var::Union{Dict{String, MOI.VariableIndex}, Nothing} - nextconstraintid::Int64 con_to_name::Dict{MOI.ConstraintIndex, String} name_to_con::Union{Dict{String, MOI.ConstraintIndex}, Nothing} - constrmap::Vector{Int} scalaraffinefunction::LPModelScalarConstraints{T, MOI.ScalarAffineFunction{T}} vectorofvariables::LPModelVectorConstraints{T, MOI.VectorOfVariables} vectoraffinefunction::LPModelVectorConstraints{T, MOI.VectorAffineFunction{T}} @@ -1181,7 +1038,7 @@ macro model( ((scalarconstraints, scalar_sets), (vectorconstraints, vector_sets)) for s in sets field = _field(s) - push!(c.args[3].args, :($field::Vector{$(_getC(s))})) + push!(c.args[3].args, :($field::$(_getC(s)))) end end @@ -1212,10 +1069,8 @@ macro model( var_to_name::Dict{$VI,String} # If `nothing`, the dictionary hasn't been constructed yet. name_to_var::Union{Dict{String,$VI},Nothing} - nextconstraintid::Int64 con_to_name::Dict{$CI,String} name_to_con::Union{Dict{String,$CI},Nothing} - constrmap::Vector{Int} # Constraint Reference value ci -> index in array in Constraints # A useful dictionary for extensions to store things. These are # _not_ copied between models! ext::Dict{Symbol,Any} @@ -1234,24 +1089,8 @@ macro model( function $MOIU.broadcastvcat(f::F, model::$esc_model_name) where {F<:Function} return vcat($(_broadcastfield.(Ref(:(broadcastvcat)), funs)...)) end - function $MOI.empty!(model::$esc_model_name{T}) where {T} - model.name = "" - model.senseset = false - model.sense = $MOI.FEASIBILITY_SENSE - model.objectiveset = false - model.objective = zero($MOI.ScalarAffineFunction{T}) - model.num_variables_created = 0 - model.variable_indices = nothing - model.single_variable_mask = UInt8[] - model.lower_bound = T[] - model.upper_bound = T[] - empty!(model.var_to_name) - model.name_to_var = nothing - model.nextconstraintid = 0 - empty!(model.con_to_name) - model.name_to_con = nothing - empty!(model.constrmap) - return $(Expr(:block, _callfield.(Ref(:($MOI.empty!)), funs)...)) + function $MOIU.mapreduce_constraints(f::Function, op::Function, model::$esc_model_name, cur) + return $(Expr(:block, _mapreduce_field.(funs)...)) end end for (cname, sets) in ((scname, scalar_sets), (vcname, vector_sets)) @@ -1263,50 +1102,39 @@ macro model( function $MOIU.broadcastvcat(f::F, model::$cname) where {F<:Function} return vcat($(_callfield.(:f, sets)...)) end - function $MOI.empty!(model::$cname) - return $(Expr(:block, _callfield.(Ref(:(Base.empty!)), sets)...)) + function $MOIU.mapreduce_constraints(f::Function, op::Function, model::$cname, cur) + return $(Expr(:block, _mapreduce_constraints.(sets)...)) end end end - for (funct, T) in ( - (:_add_constraint, CI), - (:_modify, CI), - (:_delete, CI), - (:_getindex, CI), - (:_getfunction, CI), - (:_getset, CI), - (:_getnoc, MOI.NumberOfConstraints), - ) - for (c, sets) in ((scname, scalar_sets), (vcname, vector_sets)) - for s in sets - set = _set(s) - field = _field(s) - code = quote - $code - function $MOIU.$funct( - model::$c, - ci::$T{F,<:$set}, - args..., - ) where {F} - return $funct(model.$field, ci, args...) - end + for (c, sets) in ((scname, scalar_sets), (vcname, vector_sets)) + for s in sets + set = _set(s) + field = _field(s) + code = quote + $code + function $MOIU.constraints( + model::$c, + ::Type{<:$set}, + ) where {F} + return model.$field end end end + end - for f in funs - fun = _fun(f) - field = _field(f) - code = quote - $code - function $MOIU.$funct( - model::$esc_model_name, - ci::$T{<:$fun}, - args..., - ) - return $funct(model.$field, ci, args...) - end + for f in funs + fun = _fun(f) + field = _field(f) + code = quote + $code + function $MOIU.constraints( + model::$esc_model_name, + ::Type{<:$fun}, + ::Type{S} + ) where S + return $MOIU.constraints(model.$field, S) end end end @@ -1337,10 +1165,8 @@ macro model( T[], Dict{$VI,String}(), nothing, - 0, Dict{$CI,String}(), nothing, - Int[], Dict{Symbol,Any}(), $(_getCV.(funs)...), ) diff --git a/src/Utilities/universalfallback.jl b/src/Utilities/universalfallback.jl index 678097a038..93da5fd227 100644 --- a/src/Utilities/universalfallback.jl +++ b/src/Utilities/universalfallback.jl @@ -14,8 +14,9 @@ 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/jump-dev/JuMP.jl/issues/1152 and https://github.com/jump-dev/JuMP.jl/issues/2238 - nextconstraintid::Int64 + # See https://github.com/jump-dev/JuMP.jl/issues/1152 and https://github.com/jump-dev/JuMP.jl/issues/2238 for why we use an `OrderedDict` + single_variable_constraints::OrderedDict{DataType,OrderedDict} + constraints::OrderedDict{Tuple{DataType,DataType},VectorOfConstraints} con_to_name::Dict{CI,String} name_to_con::Union{Dict{String,MOI.ConstraintIndex},Nothing} optattr::Dict{MOI.AbstractOptimizerAttribute,Any} @@ -27,7 +28,7 @@ mutable struct UniversalFallback{MT} <: MOI.ModelLike model, nothing, OrderedDict{Tuple{DataType,DataType},OrderedDict}(), - 0, + OrderedDict{Tuple{DataType,DataType},VectorOfConstraints}(), Dict{CI,String}(), nothing, Dict{MOI.AbstractOptimizerAttribute,Any}(), @@ -47,6 +48,7 @@ function Base.show(io::IO, U::UniversalFallback) MOIU.print_with_acronym(io, summary(U)) !(U.objective === nothing) && print(io, "\n$(indent)with objective") for (attr, name) in ( + (U.single_variable_constraints, "`SingleVariable` constraint"), (U.constraints, "constraint"), (U.optattr, "optimizer attribute"), (U.modattr, "model attribute"), @@ -65,6 +67,7 @@ end function MOI.is_empty(uf::UniversalFallback) return MOI.is_empty(uf.model) && uf.objective === nothing && + isempty(uf.single_variable_constraints) && isempty(uf.constraints) && isempty(uf.modattr) && isempty(uf.varattr) && @@ -73,111 +76,78 @@ end function MOI.empty!(uf::UniversalFallback) MOI.empty!(uf.model) uf.objective = nothing + empty!(uf.single_variable_constraints) empty!(uf.constraints) - uf.nextconstraintid = 0 empty!(uf.con_to_name) uf.name_to_con = nothing empty!(uf.modattr) empty!(uf.varattr) - return empty!(uf.conattr) + empty!(uf.conattr) + return end + function MOI.copy_to(uf::UniversalFallback, src::MOI.ModelLike; kws...) return MOIU.automatic_copy_to(uf, src; kws...) end + function supports_default_copy_to(uf::UniversalFallback, copy_names::Bool) return supports_default_copy_to(uf.model, copy_names) end # References -MOI.is_valid(uf::UniversalFallback, idx::VI) = MOI.is_valid(uf.model, idx) -function MOI.is_valid(uf::UniversalFallback, idx::CI{F,S}) where {F,S} - if MOI.supports_constraint(uf.model, F, S) - MOI.is_valid(uf.model, idx) +function MOI.is_valid(uf::UniversalFallback, idx::MOI.VariableIndex) + return MOI.is_valid(uf.model, idx) +end +function MOI.is_valid(uf::UniversalFallback, idx::CI{MOI.SingleVariable,S}) where {S} + if MOI.supports_constraint(uf.model, MOI.SingleVariable, S) + return MOI.is_valid(uf.model, idx) else - haskey(uf.constraints, (F, S)) && haskey(uf.constraints[(F, S)], idx) + return haskey(uf.single_variable_constraints, S) && + haskey(uf.single_variable_constraints[S], idx) end end -function MOI.delete(uf::UniversalFallback, ci::CI{F,S}) where {F,S} - if MOI.supports_constraint(uf.model, F, S) +function MOI.is_valid(uf::UniversalFallback, idx::MOI.ConstraintIndex{F,S}) where {F,S} + if !MOI.supports_constraint(uf.model, F, S) && !haskey(uf.constraints, (F, S)) + return false + end + return MOI.is_valid(constraints(uf, idx), idx) +end +function _delete(uf::UniversalFallback, ci::MOI.ConstraintIndex{MOI.SingleVariable,S}) where {S} + if MOI.supports_constraint(uf.model, MOI.SingleVariable, S) MOI.delete(uf.model, ci) else - if !MOI.is_valid(uf, ci) - throw(MOI.InvalidIndex(ci)) - end - delete!(uf.constraints[(F, S)], ci) + MOI.is_valid(uf, ci) || throw(MOI.InvalidIndex(ci)) + delete!(uf.single_variable_constraints[S], ci) + end + return +end +function _delete(uf::UniversalFallback, ci::MOI.ConstraintIndex) + MOI.delete(constraints(uf, ci), ci) + return +end +function MOI.delete(uf::UniversalFallback, ci::MOI.ConstraintIndex{F,S}) where {F,S} + _delete(uf, ci) + if !MOI.supports_constraint(uf.model, F, S) delete!(uf.con_to_name, ci) uf.name_to_con = nothing end for d in values(uf.conattr) delete!(d, ci) end + return end function _remove_variable( uf::UniversalFallback, constraints::OrderedDict{<:CI{MOI.SingleVariable}}, - vi::VI, + vi::MOI.VariableIndex, ) - to_delete = keytype(constraints)[] - for (ci, constraint) in constraints - f::MOI.SingleVariable = constraint[1] - if f.variable == vi - push!(to_delete, ci) - end - end - return MOI.delete(uf, to_delete) + return MOI.delete(uf, [ci for ci in keys(constraints) if ci.value == vi.value]) end -function _remove_variable( - uf::UniversalFallback, - constraints::OrderedDict{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 - return MOI.delete(uf, to_delete) -end -function _remove_variable( - ::UniversalFallback, - constraints::OrderedDict{<:CI}, - vi::VI, -) - for (ci, constraint) in constraints - f, s = constraint - constraints[ci] = remove_variable(f, s, vi) +function MOI.delete(uf::UniversalFallback, vi::MOI.VariableIndex) + vis = [vi] + for constraints in values(uf.constraints) + _throw_if_cannot_delete(constraints, vis, vis) end -end -function _remove_vector_of_variables( - uf::UniversalFallback, - constraints::OrderedDict{<: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 - return MOI.delete(uf, to_delete) -end -function _remove_vector_of_variables( - ::UniversalFallback, - ::OrderedDict{<: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) @@ -185,11 +155,25 @@ function MOI.delete(uf::UniversalFallback, vi::VI) if uf.objective !== nothing uf.objective = remove_variable(uf.objective, vi) end - for (_, constraints) in uf.constraints + for constraints in values(uf.single_variable_constraints) _remove_variable(uf, constraints, vi) end + for constraints in values(uf.constraints) + _deleted_constraints(constraints, vi) do ci + delete!(uf.con_to_name, ci) + uf.name_to_con = nothing + for d in values(uf.conattr) + delete!(d, ci) + end + end + end + return end -function MOI.delete(uf::UniversalFallback, vis::Vector{VI}) +function MOI.delete(uf::UniversalFallback, vis::Vector{MOI.VariableIndex}) + fast_in_vis = Set(vis) + for constraints in values(uf.constraints) + _throw_if_cannot_delete(constraints, vis, fast_in_vis) + end MOI.delete(uf.model, vis) for d in values(uf.varattr) for vi in vis @@ -199,12 +183,21 @@ function MOI.delete(uf::UniversalFallback, vis::Vector{VI}) if uf.objective !== nothing uf.objective = remove_variable(uf.objective, vis) end - for (_, constraints) in uf.constraints - _remove_vector_of_variables(uf, constraints, vis) + for constraints in values(uf.single_variable_constraints) for vi in vis _remove_variable(uf, constraints, vi) end end + for constraints in values(uf.constraints) + _deleted_constraints(constraints, vis) do ci + delete!(uf.con_to_name, ci) + uf.name_to_con = nothing + for d in values(uf.conattr) + delete!(d, ci) + end + end + end + return end # Attributes @@ -234,21 +227,15 @@ function _get( ci::MOI.ConstraintIndex, ) return MOI.get_fallback(uf, attr, ci) - func = MOI.get(uf, MOI.ConstraintFunction(), ci) - if is_canonical(func) - return func - else - return canonical(func) - end end function MOI.get( uf::UniversalFallback, attr::Union{MOI.AbstractOptimizerAttribute,MOI.AbstractModelAttribute}, ) if !MOI.is_copyable(attr) || MOI.supports(uf.model, attr) - MOI.get(uf.model, attr) + return MOI.get(uf.model, attr) else - _get(uf, attr) + return _get(uf, attr) end end function MOI.get( @@ -258,9 +245,9 @@ function MOI.get( ) where {F,S} if MOI.supports_constraint(uf.model, F, S) && (!MOI.is_copyable(attr) || MOI.supports(uf.model, attr, typeof(idx))) - MOI.get(uf.model, attr, idx) + return MOI.get(uf.model, attr, idx) else - _get(uf, attr, idx) + return _get(uf, attr, idx) end end function MOI.get( @@ -269,43 +256,62 @@ function MOI.get( idx::MOI.VariableIndex, ) if !MOI.is_copyable(attr) || MOI.supports(uf.model, attr, typeof(idx)) - MOI.get(uf.model, attr, idx) + return MOI.get(uf.model, attr, idx) else - _get(uf, attr, idx) + return _get(uf, attr, idx) end end function MOI.get( uf::UniversalFallback, - attr::MOI.NumberOfConstraints{F,S}, -) where {F,S} + attr::MOI.NumberOfConstraints{MOI.SingleVariable,S}, +) where {S} + F = MOI.SingleVariable if MOI.supports_constraint(uf.model, F, S) return MOI.get(uf.model, attr) else return length(get( - uf.constraints, - (F, S), - OrderedDict{CI{F,S},Tuple{F,S}}(), + uf.single_variable_constraints, + S, + OrderedDict{CI{F,S},S}(), )) end end function MOI.get( uf::UniversalFallback, - listattr::MOI.ListOfConstraintIndices{F,S}, + attr::MOI.NumberOfConstraints{F,S}, ) where {F,S} + return MOI.get(constraints(uf, F, S), attr) +end +function MOI.get( + uf::UniversalFallback, + listattr::MOI.ListOfConstraintIndices{MOI.SingleVariable,S}, +) where {S} + F = MOI.SingleVariable if MOI.supports_constraint(uf.model, F, S) - MOI.get(uf.model, listattr) + return MOI.get(uf.model, listattr) else - collect(keys(get( - uf.constraints, - (F, S), - OrderedDict{CI{F,S},Tuple{F,S}}(), + return collect(keys(get( + uf.single_variable_constraints, + S, + OrderedDict{CI{F,S},S}(), ))) end end +function MOI.get( + uf::UniversalFallback, + listattr::MOI.ListOfConstraintIndices{F,S}, +) where {F,S} + return MOI.get(constraints(uf, F, S), listattr) +end function MOI.get(uf::UniversalFallback, listattr::MOI.ListOfConstraints) list = MOI.get(uf.model, listattr) - for (FS, constraints) in uf.constraints + for (S, constraints) in uf.single_variable_constraints if !isempty(constraints) + push!(list, (MOI.SingleVariable, S)) + end + end + for (FS, constraints) in uf.constraints + if !MOI.is_empty(constraints) push!(list, FS) end end @@ -361,7 +367,8 @@ function MOI.set( if sense == MOI.FEASIBILITY_SENSE uf.objective = nothing end - return MOI.set(uf.model, attr, sense) + MOI.set(uf.model, attr, sense) + return end function MOI.get(uf::UniversalFallback, attr::MOI.ObjectiveFunctionType) if uf.objective === nothing @@ -396,6 +403,7 @@ function MOI.set( MOI.set(uf.model, MOI.ObjectiveSense(), MOI.FEASIBILITY_SENSE) MOI.set(uf.model, MOI.ObjectiveSense(), sense) end + return end function MOI.modify( @@ -408,6 +416,7 @@ function MOI.modify( else uf.objective = modify_function(uf.objective, change) end + return end # Name @@ -568,111 +577,130 @@ end # Constraints function MOI.supports_constraint( uf::UniversalFallback, - ::Type{F}, - ::Type{S}, -) where {F<:MOI.AbstractFunction,S<:MOI.AbstractSet} + ::Type{<:MOI.AbstractFunction}, + ::Type{<:MOI.AbstractSet}, +) return true end -function _new_constraint_index( - uf, - f::MOI.SingleVariable, - s::MOI.AbstractScalarSet, -) - return CI{MOI.SingleVariable,typeof(s)}(f.variable.value) +function constraints( + uf::UniversalFallback, + ::Type{F}, + ::Type{S}, + getter::Function=get, +) where {F,S} + if MOI.supports_constraint(uf.model, F, S) + return uf.model + else + return getter(uf.constraints, (F, S)) do + return VectorOfConstraints{F,S}() + end::VectorOfConstraints{F,S} + end end -function _new_constraint_index(uf, f::MOI.AbstractFunction, s::MOI.AbstractSet) - uf.nextconstraintid += 1 - return CI{typeof(f),typeof(s)}(uf.nextconstraintid) +function constraints( + uf::UniversalFallback, + ci::MOI.ConstraintIndex{F,S} +) where {F,S} + if !MOI.supports_constraint(uf, F, S) + throw(MOI.InvalidIndex(ci)) + end + return constraints(uf, F, S) end function MOI.add_constraint( uf::UniversalFallback, - f::MOI.AbstractFunction, - s::MOI.AbstractSet, -) - F = typeof(f) - S = typeof(s) - if MOI.supports_constraint(uf.model, F, S) - return MOI.add_constraint(uf.model, f, s) + func::MOI.SingleVariable, + set::S, +) where S <: MOI.AbstractScalarSet + if MOI.supports_constraint(uf.model, MOI.SingleVariable, S) + return MOI.add_constraint(uf.model, func, set) else constraints = - get!(uf.constraints, (F, S)) do - return OrderedDict{ - CI{F,S}, - Tuple{F,S}, - }() - end::OrderedDict{CI{F,S},Tuple{F,S}} - ci = _new_constraint_index(uf, canonical(f), copy(s)) - constraints[ci] = (f, s) + get!(uf.single_variable_constraints, S) do + return OrderedDict{CI{MOI.SingleVariable,S},S,}() + end::OrderedDict{CI{MOI.SingleVariable,S},S} + ci = MOI.ConstraintIndex{MOI.SingleVariable,S}(func.variable.value) + constraints[ci] = set return ci end end +function MOI.add_constraint( + uf::UniversalFallback, + func::MOI.AbstractFunction, + set::MOI.AbstractSet, +) + return MOI.add_constraint( + constraints(uf, typeof(func), typeof(set), get!), + func, + set, + ) +end function MOI.modify( uf::UniversalFallback, - ci::CI{F,S}, + ci::MOI.ConstraintIndex, change::MOI.AbstractFunctionModification, -) where {F,S} - if MOI.supports_constraint(uf.model, F, S) - MOI.modify(uf.model, ci, change) - else - (f, s) = uf.constraints[(F, S)][ci] - uf.constraints[(F, S)][ci] = (modify_function(f, change), s) - end +) + MOI.modify(constraints(uf, ci), ci, change) + return end function MOI.get( uf::UniversalFallback, - attr::MOI.ConstraintFunction, - ci::CI{F,S}, -) where {F,S} - if MOI.supports_constraint(uf.model, F, S) - MOI.get(uf.model, attr, ci) - else - MOI.throw_if_not_valid(uf, ci) - uf.constraints[(F, S)][ci][1] - end + attr::Union{MOI.ConstraintFunction, MOI.ConstraintSet}, + ci::MOI.ConstraintIndex, +) + return MOI.get(constraints(uf, ci), attr, ci) +end + +function MOI.set( + uf::UniversalFallback, + attr::Union{MOI.ConstraintFunction, MOI.ConstraintSet}, + ci::MOI.ConstraintIndex, + func_or_set, +) + return MOI.set(constraints(uf, ci), attr, ci, func_or_set) +end + +function MOI.get( + uf::UniversalFallback, + ::MOI.ConstraintFunction, + ci::CI{MOI.SingleVariable}, +) + MOI.throw_if_not_valid(uf, ci) + return MOI.SingleVariable(MOI.VariableIndex(ci.value)) end function MOI.get( uf::UniversalFallback, - attr::MOI.ConstraintSet, - ci::CI{F,S}, -) where {F,S} - if MOI.supports_constraint(uf.model, F, S) - MOI.get(uf.model, attr, ci) + ::MOI.ConstraintSet, + ci::MOI.ConstraintIndex{MOI.SingleVariable,S}, +) where {S} + if MOI.supports_constraint(uf.model, MOI.SingleVariable, S) + MOI.get(uf.model, MOI.ConstraintSet(), ci) else MOI.throw_if_not_valid(uf, ci) - uf.constraints[(F, S)][ci][2] + return uf.single_variable_constraints[S][ci] end end + function MOI.set( uf::UniversalFallback, - ::MOI.ConstraintFunction, - ci::CI{F,S}, - func::F, -) where {F,S} - if MOI.supports_constraint(uf.model, F, S) - MOI.set(uf.model, MOI.ConstraintFunction(), ci, func) - else - MOI.throw_if_not_valid(uf, ci) - if F == MOI.SingleVariable - throw(MOI.SettingSingleVariableFunctionNotAllowed()) - end - (_, s) = uf.constraints[(F, S)][ci] - uf.constraints[(F, S)][ci] = (func, s) - end + attr::MOI.ConstraintFunction, + ci::MOI.ConstraintIndex{MOI.SingleVariable}, + func::MOI.SingleVariable, +) + return throw(MOI.SettingSingleVariableFunctionNotAllowed()) end function MOI.set( uf::UniversalFallback, ::MOI.ConstraintSet, - ci::CI{F,S}, + ci::MOI.ConstraintIndex{MOI.SingleVariable,S}, set::S, -) where {F,S} - if MOI.supports_constraint(uf.model, F, S) +) where {S} + if MOI.supports_constraint(uf.model, MOI.SingleVariable, S) MOI.set(uf.model, MOI.ConstraintSet(), ci, set) else MOI.throw_if_not_valid(uf, ci) - (f, _) = uf.constraints[(F, S)][ci] - uf.constraints[(F, S)][ci] = (f, set) + uf.single_variable_constraints[S][ci] = set end + return end # Variables diff --git a/src/Utilities/vector_of_constraints.jl b/src/Utilities/vector_of_constraints.jl new file mode 100644 index 0000000000..9b8af523a0 --- /dev/null +++ b/src/Utilities/vector_of_constraints.jl @@ -0,0 +1,240 @@ +## Storage of constraints +# +# All `F`-in-`S` constraints are stored in a vector of `ConstraintEntry{F, S}`. +# The index in this vector of a constraint of index +# `ci::MOI.ConstraintIndex{F, S}` is given by `model.constrmap[ci.value]`. The +# advantage of this representation is that it does not require any dictionary +# hence it never needs to compute a hash. +# +# It may seem redundant to store the constraint index `ci` as well as the +# function and sets in the tuple but it is used to efficiently implement the +# getter for `MOI.ListOfConstraintIndices{F, S}`. It is also used to implement +# `MOI.delete`. Indeed, when a constraint is deleted, it is removed from the +# vector hence the index in the vector of all the functions that were stored +# after must be decreased by one. As the constraint index is stored in the +# vector, it readily gives the entries of `model.constrmap` that need to be +# updated. + +struct VectorOfConstraints{ + F<:MOI.AbstractFunction, + S<:MOI.AbstractSet +} <: MOI.ModelLike + # FIXME: It is not ideal that we have `DataType` here, it might induce type + # instabilities. We should change `CleverDicts` so that we can just + # use `typeof(CleverDicts.index_to_key)` here. + constraints::CleverDicts.CleverDict{ + MOI.ConstraintIndex{F,S}, + Tuple{F,S}, + typeof(CleverDicts.key_to_index), + Base.Fix1{typeof(CleverDicts.index_to_key),DataType} + } + + function VectorOfConstraints{F,S}() where {F,S} + return new{F,S}( + CleverDicts.CleverDict{MOI.ConstraintIndex{F,S},Tuple{F,S}}() + ) + end +end + +MOI.is_empty(v::VectorOfConstraints) = isempty(v.constraints) +MOI.empty!(v::VectorOfConstraints) = empty!(v.constraints) + +function MOI.add_constraint( + v::VectorOfConstraints{F,S}, + func::F, + set::S, +) where {F<:MOI.AbstractFunction,S<:MOI.AbstractSet} + # We canonicalize the constraint so that solvers can avoid having to + # canonicalize it most of the time (they can check if they need to with + # `is_canonical`. + # Note that the canonicalization is not guaranteed if for instance + # `modify` is called and adds a new term. + # See https://github.com/jump-dev/MathOptInterface.jl/pull/1118 + return CleverDicts.add_item(v.constraints, (canonical(func), copy(set))) +end + +function MOI.is_valid( + v::VectorOfConstraints{F,S}, + ci::MOI.ConstraintIndex{F,S}, +) where {F,S} + return haskey(v.constraints, ci) +end + +function MOI.delete( + v::VectorOfConstraints{F,S}, + ci::MOI.ConstraintIndex{F,S}, +) where {F,S} + MOI.throw_if_not_valid(v, ci) + delete!(v.constraints, ci) + return +end + +function MOI.get( + v::VectorOfConstraints{F,S}, + ::MOI.ConstraintFunction, + ci::MOI.ConstraintIndex{F,S}, +) where {F,S} + MOI.throw_if_not_valid(v, ci) + return v.constraints[ci][1] +end + +function MOI.get( + v::VectorOfConstraints{F,S}, + ::MOI.ConstraintSet, + ci::MOI.ConstraintIndex{F,S}, +) where {F,S} + MOI.throw_if_not_valid(v, ci) + return v.constraints[ci][2] +end + +function MOI.set( + v::VectorOfConstraints{F,S}, + ::MOI.ConstraintFunction, + ci::MOI.ConstraintIndex{F,S}, + func::F, +) where {F,S} + MOI.throw_if_not_valid(v, ci) + v.constraints[ci] = (func, v.constraints[ci][2]) + return +end + +function MOI.set( + v::VectorOfConstraints{F,S}, + ::MOI.ConstraintSet, + ci::MOI.ConstraintIndex{F,S}, + set::S, +) where {F,S} + MOI.throw_if_not_valid(v, ci) + v.constraints[ci] = (v.constraints[ci][1], set) + return +end + +function MOI.get( + v::VectorOfConstraints{F,S}, + ::MOI.ListOfConstraints, +)::Vector{Tuple{DataType,DataType}} where {F,S} + return isempty(v.constraints) ? [] : [(F, S)] +end + +function MOI.get( + v::VectorOfConstraints{F,S}, + ::MOI.NumberOfConstraints{F,S}, +) where {F,S} + return length(v.constraints) +end + +function MOI.get( + v::VectorOfConstraints{F,S}, + ::MOI.ListOfConstraintIndices{F,S}, +) where {F,S} + return keys(v.constraints) +end + +function MOI.modify( + v::VectorOfConstraints{F,S}, + ci::MOI.ConstraintIndex{F,S}, + change::MOI.AbstractFunctionModification, +) where {F,S} + func, set = v.constraints[ci] + v.constraints[ci] = (modify_function(func, change), set) + return +end + +# Deletion of variables in vector of variables + +function _remove_variable(v::VectorOfConstraints, vi::MOI.VariableIndex) + CleverDicts.map_values!(v.constraints) do func_set + return remove_variable(func_set..., vi) + end + return +end +function _filter_variables(keep::Function, v::VectorOfConstraints) + CleverDicts.map_values!(v.constraints) do func_set + return filter_variables(keep, func_set...) + end + return +end + +function throw_delete_variable_in_vov(vi::MOI.VariableIndex) + message = string( + "Cannot delete variable as it is constrained with other", + " variables in a `MOI.VectorOfVariables`.", + ) + return throw(MOI.DeleteNotAllowed(vi, message)) +end + +# Nothing to do as it's not `VectorOfVariables` constraints +_throw_if_cannot_delete(::VectorOfConstraints, vis, fast_in_vis) = nothing + +function _throw_if_cannot_delete( + v::VectorOfConstraints{MOI.VectorOfVariables,S}, + vis, + fast_in_vis, +) where {S<:MOI.AbstractVectorSet} + if MOI.supports_dimension_update(S) + return + end + for fs in values(v.constraints) + f = fs[1]::MOI.VectorOfVariables + if length(f.variables) > 1 && f.variables != vis + for vi in f.variables + if vi in fast_in_vis + # If `supports_dimension_update(S)` then the variable + # will be removed in `_filter_variables`. + throw_delete_variable_in_vov(vi) + end + end + end + end + return +end + +function _delete_variables( + ::Function, + ::VectorOfConstraints, + ::Vector{MOI.VariableIndex}, +) + return # Nothing to do as it's not `VectorOfVariables` constraints +end + +function _delete_variables( + callback::Function, + v::VectorOfConstraints{MOI.VectorOfVariables,S}, + vis::Vector{MOI.VariableIndex}, +) where {S<:MOI.AbstractVectorSet} + filter!(v.constraints) do p + f = p.second[1] + del = if length(f.variables) == 1 + first(f.variables) in vis + else + vis == f.variables + end + if del + callback(p.first) + end + return !del + end + return +end + +function _deleted_constraints( + callback::Function, + v::VectorOfConstraints, + vi::MOI.VariableIndex, +) + vis = [vi] + _delete_variables(callback, v, vis) + _remove_variable(v, vi) + return +end + +function _deleted_constraints( + callback::Function, + v::VectorOfConstraints, + vis::Vector{MOI.VariableIndex}, +) + removed = Set(vis) + _delete_variables(callback, v, vis) + _filter_variables(vi -> !(vi in removed), v) + return +end diff --git a/test/Utilities/model.jl b/test/Utilities/model.jl index fa7c24ed80..18fa95cb85 100644 --- a/test/Utilities/model.jl +++ b/test/Utilities/model.jl @@ -219,8 +219,8 @@ end loc1 = MOI.get(model, MOI.ListOfConstraints()) loc2 = Vector{Tuple{DataType, DataType}}() - function _pushloc(constrs::Vector{MOIU.ConstraintEntry{F, S}}) where {F, S} - if !isempty(constrs) + function _pushloc(v::MOI.Utilities.VectorOfConstraints{F, S}) where {F, S} + if !MOI.is_empty(v) push!(loc2, (F, S)) end end diff --git a/test/Utilities/universalfallback.jl b/test/Utilities/universalfallback.jl index 12dd4c720d..a59449fd2e 100644 --- a/test/Utilities/universalfallback.jl +++ b/test/Utilities/universalfallback.jl @@ -110,6 +110,7 @@ end @testset "Optimizer Attribute" begin attr = UnknownOptimizerAttribute() listattr = MOI.ListOfOptimizerAttributesSet() + empty!(uf.optattr) test_optmodattrs(uf, model, attr, listattr) end @testset "Model Attribute" begin @@ -262,8 +263,9 @@ end # 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 - @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)] + for set in sets + @test MOI.get(uf, MOI.ListOfConstraintIndices{F, typeof(set)}()) == [MOI.ConstraintIndex{F, typeof(set)}(1), MOI.ConstraintIndex{F, typeof(set)}(2)] + end end end