diff --git a/docs/src/submodules/Utilities/overview.md b/docs/src/submodules/Utilities/overview.md index fbeb038084..80a0084fb4 100644 --- a/docs/src/submodules/Utilities/overview.md +++ b/docs/src/submodules/Utilities/overview.md @@ -410,7 +410,7 @@ const Model = MOI.Utilities.GenericModel{ MOI.Utilities.MatrixOfConstraints{ Float64, MOI.Utilities.MutableSparseMatrixCSC{Float64,Cint,MOI.Utilities.ZeroBasedIndexing}, - MOI.Utilities.Box{Float64}, + MOI.Utilities.Hyperrectangle{Float64}, LP{Float64}, }, } diff --git a/docs/src/submodules/Utilities/reference.md b/docs/src/submodules/Utilities/reference.md index dc5fc69737..9f6a7f7dc4 100644 --- a/docs/src/submodules/Utilities/reference.md +++ b/docs/src/submodules/Utilities/reference.md @@ -113,7 +113,7 @@ Utilities.set_from_constants ``` ```@docs -Utilities.Box +Utilities.Hyperrectangle ``` ### `.sets` diff --git a/src/Utilities/Utilities.jl b/src/Utilities/Utilities.jl index 71cfcf0e5d..6782ae72e5 100644 --- a/src/Utilities/Utilities.jl +++ b/src/Utilities/Utilities.jl @@ -59,7 +59,7 @@ include("copy.jl") include("results.jl") include("variables.jl") -include("box.jl") +include("vector_bounds.jl") include("vector_of_constraints.jl") include("struct_of_constraints.jl") include("model.jl") diff --git a/src/Utilities/box.jl b/src/Utilities/box.jl deleted file mode 100644 index b5915a05d4..0000000000 --- a/src/Utilities/box.jl +++ /dev/null @@ -1,132 +0,0 @@ -# Sets setting lower bound: -extract_lower_bound(set::MOI.EqualTo) = set.value -function extract_lower_bound( - set::Union{MOI.GreaterThan,MOI.Interval,MOI.Semicontinuous,MOI.Semiinteger}, -) - return set.lower -end -# 0xcb = 0x80 | 0x40 | 0x8 | 0x2 | 0x1 -const LOWER_BOUND_MASK = 0xcb - -# Sets setting upper bound: -extract_upper_bound(set::MOI.EqualTo) = set.value -function extract_upper_bound( - set::Union{MOI.LessThan,MOI.Interval,MOI.Semicontinuous,MOI.Semiinteger}, -) - return set.upper -end -# 0xcd = 0x80 | 0x40 | 0x8 | 0x4 | 0x1 -const UPPER_BOUND_MASK = 0xcd - -const SUPPORTED_VARIABLE_SCALAR_SETS{T} = Union{ - MOI.EqualTo{T}, - MOI.GreaterThan{T}, - MOI.LessThan{T}, - MOI.Interval{T}, - MOI.Integer, - MOI.ZeroOne, - MOI.Semicontinuous{T}, - MOI.Semiinteger{T}, -} - -""" - struct Box{T} - lower::Vector{T} - upper::Vector{T} - end - -Stores the constants of scalar constraints with the lower bound of the set in -`lower` and the upper bound in `upper`. -""" -struct Box{T} - lower::Vector{T} - upper::Vector{T} -end - -Box{T}() where {T} = Box{T}(T[], T[]) - -Base.:(==)(a::Box, b::Box) = a.lower == b.lower && a.upper == b.upper - -function Base.empty!(b::Box) - empty!(b.lower) - empty!(b.upper) - return b -end - -function Base.resize!(b::Box, n) - resize!(b.lower, n) - resize!(b.upper, n) - return -end - -function add_free(b::Box{T}) where {T} - push!(b.lower, _no_lower_bound(T)) - push!(b.upper, _no_upper_bound(T)) - return -end - -# Use `-Inf` and `Inf` for `AbstractFloat` subtypes. -_no_lower_bound(::Type{T}) where {T} = zero(T) -_no_lower_bound(::Type{T}) where {T<:AbstractFloat} = typemin(T) -_no_upper_bound(::Type{T}) where {T} = zero(T) -_no_upper_bound(::Type{T}) where {T<:AbstractFloat} = typemax(T) - -function load_constants( - b::Box{T}, - offset, - set::SUPPORTED_VARIABLE_SCALAR_SETS{T}, -) where {T} - flag = single_variable_flag(typeof(set)) - if iszero(flag & LOWER_BOUND_MASK) - b.lower[offset+1] = _no_lower_bound(T) - else - b.lower[offset+1] = extract_lower_bound(set) - end - if iszero(flag & UPPER_BOUND_MASK) - b.upper[offset+1] = _no_upper_bound(T) - else - b.upper[offset+1] = extract_upper_bound(set) - end - return -end - -function merge_bounds(b::Box, index, set) - flag = single_variable_flag(typeof(set)) - if !iszero(flag & LOWER_BOUND_MASK) - b.lower[index] = extract_lower_bound(set) - end - if !iszero(flag & UPPER_BOUND_MASK) - b.upper[index] = extract_upper_bound(set) - end -end - -function_constants(::Box{T}, row) where {T} = zero(T) - -function set_from_constants(b::Box, ::Type{<:MOI.EqualTo}, index) - return MOI.EqualTo(b.lower[index]) -end -function set_from_constants( - b::Box, - S::Type{<:Union{MOI.GreaterThan,MOI.EqualTo}}, - index, -) - # Lower and upper bounds are equal for `EqualTo`, we can take either of them. - return S(b.lower[index]) -end -function set_from_constants(b::Box, S::Type{<:MOI.LessThan}, index) - return S(b.upper[index]) -end -function set_from_constants( - b::Box, - S::Type{<:Union{MOI.Interval,MOI.Semicontinuous,MOI.Semiinteger}}, - index, -) - return S(b.lower[index], b.upper[index]) -end -function set_from_constants( - ::Box, - S::Type{<:Union{MOI.Integer,MOI.ZeroOne}}, - index, -) - return S() -end diff --git a/src/Utilities/model.jl b/src/Utilities/model.jl index 1fad5751e1..bb7d389217 100644 --- a/src/Utilities/model.jl +++ b/src/Utilities/model.jl @@ -6,12 +6,8 @@ abstract type AbstractOptimizer{T} <: MOI.AbstractOptimizer end const AbstractModel{T} = Union{AbstractModelLike{T},AbstractOptimizer{T}} # Variables -function MOI.get(model::AbstractModel, ::MOI.NumberOfVariables)::Int64 - if model.variable_indices === nothing - return model.num_variables_created - else - return length(model.variable_indices) - end +function MOI.get(model::AbstractModel, attr::MOI.NumberOfVariables)::Int64 + return MOI.get(model.variable_bounds, attr) end """ @@ -25,15 +21,11 @@ function _add_variable end function _add_variable(::Nothing) end function _add_variables(::Nothing, ::Int64) end -function MOI.add_variable(model::AbstractModel{T}) where {T} - vi = VI(model.num_variables_created += 1) - push!(model.single_variable_mask, 0x0) - add_free(model.variable_bounds) - if model.variable_indices !== nothing - push!(model.variable_indices, vi) - end + +function MOI.add_variable(model::AbstractModel) + x = MOI.add_variable(model.variable_bounds) _add_variable(model.constraints) - return vi + return x end function MOI.add_variables(model::AbstractModel, n::Integer) @@ -76,17 +68,12 @@ function filter_variables( end return g, t end + function _delete_variable( model::AbstractModel{T}, vi::MOI.VariableIndex, ) where {T} - MOI.throw_if_not_valid(model, vi) - model.single_variable_mask[vi.value] = 0x0 - if model.variable_indices === nothing - model.variable_indices = - Set(MOI.get(model, MOI.ListOfVariableIndices())) - end - delete!(model.variable_indices, vi) + MOI.delete(model.variable_bounds, vi) model.name_to_var = nothing delete!(model.var_to_name, vi) model.name_to_con = nothing @@ -139,8 +126,6 @@ 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 _throw_if_cannot_delete(model.constraints, vis, Set(vis)) @@ -150,7 +135,7 @@ function MOI.delete(model::AbstractModel, vis::Vector{MOI.VariableIndex}) for vi in vis _delete_variable(model, vi) end - keep(vi::MOI.VariableIndex) = vi in model.variable_indices + keep = x -> MOI.is_valid(model, x) model.objective = filter_variables(keep, model.objective) model.name_to_con = nothing return @@ -160,31 +145,19 @@ function MOI.is_valid( model::AbstractModel, ci::CI{MOI.SingleVariable,S}, ) where {S} - return 1 ≤ ci.value ≤ length(model.single_variable_mask) && - !iszero( - model.single_variable_mask[ci.value] & single_variable_flag(S), - ) + return MOI.is_valid(model.variable_bounds, ci) end + function MOI.is_valid(model::AbstractModel, ci::MOI.ConstraintIndex) return MOI.is_valid(model.constraints, ci) end -function MOI.is_valid(model::AbstractModel, vi::VI) - if model.variable_indices === nothing - return 1 ≤ vi.value ≤ model.num_variables_created - else - return in(vi, model.variable_indices) - end +function MOI.is_valid(model::AbstractModel, x::MOI.VariableIndex) + return MOI.is_valid(model.variable_bounds, x) end -function MOI.get(model::AbstractModel, ::MOI.ListOfVariableIndices) - if model.variable_indices === nothing - return VI.(1:model.num_variables_created) - else - vis = collect(model.variable_indices) - sort!(vis, by = vi -> vi.value) # It needs to be sorted by order of creation - return vis - end +function MOI.get(model::AbstractModel, attr::MOI.ListOfVariableIndices) + return MOI.get(model.variable_bounds, attr) end # Names @@ -410,73 +383,6 @@ function MOI.get( end # Constraints -single_variable_flag(::Type{<:MOI.EqualTo}) = 0x1 -single_variable_flag(::Type{<:MOI.GreaterThan}) = 0x2 -single_variable_flag(::Type{<:MOI.LessThan}) = 0x4 -single_variable_flag(::Type{<:MOI.Interval}) = 0x8 -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, ::Type{T}) where {T} - if flag == 0x1 - return MOI.EqualTo{T} - elseif flag == 0x2 - return MOI.GreaterThan{T} - elseif flag == 0x4 - return MOI.LessThan{T} - elseif flag == 0x8 - return MOI.Interval{T} - elseif flag == 0x10 - return MOI.Integer - elseif flag == 0x20 - return MOI.ZeroOne - elseif flag == 0x40 - return MOI.Semicontinuous{T} - else - @assert flag == 0x80 - return MOI.Semiinteger{T} - end -end - -# Julia doesn't infer `S1` correctly, so we use a function barrier to improve -# inference. -function _throw_if_lower_bound_set(variable, S2, mask, T) - S1 = flag_to_set_type(mask, T) - throw(MOI.LowerBoundAlreadySet{S1,S2}(variable)) - return -end - -function throw_if_lower_bound_set(variable, S2, mask, T) - lower_mask = mask & LOWER_BOUND_MASK - if iszero(lower_mask) - return # No lower bound set. - elseif iszero(single_variable_flag(S2) & LOWER_BOUND_MASK) - return # S2 isn't related to the lower bound. - end - return _throw_if_lower_bound_set(variable, S2, lower_mask, T) -end - -# Julia doesn't infer `S1` correctly, so we use a function barrier to improve -# inference. -function _throw_if_upper_bound_set(variable, S2, mask, T) - S1 = flag_to_set_type(mask, T) - throw(MOI.UpperBoundAlreadySet{S1,S2}(variable)) - return -end - -function throw_if_upper_bound_set(variable, S2, mask, T) - upper_mask = mask & UPPER_BOUND_MASK - if iszero(upper_mask) - return # No upper bound set. - elseif iszero(single_variable_flag(S2) & UPPER_BOUND_MASK) - return # S2 isn't related to the upper bound. - end - return _throw_if_upper_bound_set(variable, S2, upper_mask, T) -end function MOI.supports_constraint( ::AbstractModel{T}, @@ -498,15 +404,7 @@ function MOI.add_constraint( f::MOI.SingleVariable, s::SUPPORTED_VARIABLE_SCALAR_SETS{T}, ) where {T} - flag = single_variable_flag(typeof(s)) - index = f.variable.value - mask = model.single_variable_mask[index] - throw_if_lower_bound_set(f.variable, typeof(s), mask, T) - throw_if_upper_bound_set(f.variable, typeof(s), mask, T) - # No error should be thrown now, we can modify `model`. - merge_bounds(model.variable_bounds, index, s) - model.single_variable_mask[index] = mask | flag - return CI{MOI.SingleVariable,typeof(s)}(index) + return MOI.add_constraint(model.variable_bounds, f, s) end function MOI.add_constraint( @@ -526,18 +424,11 @@ function MOI.get( end function _delete_constraint( - model::AbstractModel{T}, + model::AbstractModel, ci::MOI.ConstraintIndex{MOI.SingleVariable,S}, -) where {T,S} +) where {S} MOI.throw_if_not_valid(model, ci) - flag = single_variable_flag(S) - model.single_variable_mask[ci.value] &= ~flag - if !iszero(flag & LOWER_BOUND_MASK) - model.variable_bounds.lower[ci.value] = _no_lower_bound(T) - end - if !iszero(flag & UPPER_BOUND_MASK) - model.variable_bounds.upper[ci.value] = _no_upper_bound(T) - end + MOI.delete(model.variable_bounds, ci) return end @@ -569,14 +460,15 @@ function MOI.set( ) return throw(MOI.SettingSingleVariableFunctionNotAllowed()) end + function MOI.set( model::AbstractModel{T}, - ::MOI.ConstraintSet, + attr::MOI.ConstraintSet, ci::MOI.ConstraintIndex{MOI.SingleVariable,S}, set::S, ) where {T,S<:SUPPORTED_VARIABLE_SCALAR_SETS{T}} MOI.throw_if_not_valid(model, ci) - merge_bounds(model.variable_bounds, ci.value, set) + MOI.set(model.variable_bounds, attr, ci, set) return end @@ -602,11 +494,11 @@ end function MOI.get( model::AbstractModel, - ::MOI.NumberOfConstraints{MOI.SingleVariable,S}, + attr::MOI.NumberOfConstraints{MOI.SingleVariable,S}, ) where {S} - flag = single_variable_flag(S) - return count(mask -> !iszero(flag & mask), model.single_variable_mask) + return MOI.get(model.variable_bounds, attr) end + function MOI.get( model::AbstractModel, noc::MOI.NumberOfConstraints{F,S}, @@ -614,45 +506,21 @@ function MOI.get( return MOI.get(model.constraints, noc) end -function _add_constraint_type( - list, - model::AbstractModel, - S::Type{<:MOI.AbstractScalarSet}, -) - flag = single_variable_flag(S)::UInt8 - if any(mask -> !iszero(flag & mask), model.single_variable_mask) - push!(list, (MOI.SingleVariable, S)) - end - return -end function MOI.get( model::AbstractModel{T}, attr::MOI.ListOfConstraintTypesPresent, ) where {T} - list = MOI.get(model.constraints, attr)::Vector{Tuple{DataType,DataType}} - _add_constraint_type(list, model, MOI.EqualTo{T}) - _add_constraint_type(list, model, MOI.GreaterThan{T}) - _add_constraint_type(list, model, MOI.LessThan{T}) - _add_constraint_type(list, model, MOI.Interval{T}) - _add_constraint_type(list, model, MOI.Semicontinuous{T}) - _add_constraint_type(list, model, MOI.Semiinteger{T}) - _add_constraint_type(list, model, MOI.Integer) - _add_constraint_type(list, model, MOI.ZeroOne) - return list + return vcat( + MOI.get(model.constraints, attr)::Vector{Tuple{DataType,DataType}}, + MOI.get(model.variable_bounds, attr)::Vector{Tuple{DataType,DataType}}, + ) end function MOI.get( model::AbstractModel, - ::MOI.ListOfConstraintIndices{MOI.SingleVariable,S}, + attr::MOI.ListOfConstraintIndices{MOI.SingleVariable,S}, ) where {S} - list = CI{MOI.SingleVariable,S}[] - flag = single_variable_flag(S) - for (index, mask) in enumerate(model.single_variable_mask) - if !iszero(mask & flag) - push!(list, CI{MOI.SingleVariable,S}(index)) - end - end - return list + return MOI.get(model.variable_bounds, attr) end function MOI.get( @@ -693,19 +561,17 @@ function MOI.is_empty(model::AbstractModel) !model.objectiveset && isempty(model.objective.terms) && iszero(model.objective.constant) && - iszero(model.num_variables_created) && - MOI.is_empty(model.constraints) + MOI.is_empty(model.constraints) && + MOI.is_empty(model.variable_bounds) 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[] - empty!(model.variable_bounds) + MOI.empty!(model.variable_bounds) empty!(model.var_to_name) model.name_to_var = nothing empty!(model.con_to_name) @@ -990,15 +856,7 @@ for (loop_name, loop_super_type) in [ MOI.ScalarAffineFunction{T}, MOI.ScalarQuadraticFunction{T}, } - num_variables_created::Int64 - # If nothing, no variable has been deleted so the indices of the - # variables are VI.(1:num_variables_created) - variable_indices::Union{Nothing,Set{VI}} - # Union of flags of `S` such that a `SingleVariable`-in-`S` - # constraint was added to the model and not deleted yet. - single_variable_mask::Vector{UInt8} - # Bounds set by `SingleVariable`-in-`S`: - variable_bounds::Box{T} + variable_bounds::SingleVariableConstraints{T} constraints::C var_to_name::Dict{MOI.VariableIndex,String} # If `nothing`, the dictionary hasn't been constructed yet. @@ -1015,10 +873,7 @@ for (loop_name, loop_super_type) in [ MOI.FEASIBILITY_SENSE, false, zero(MOI.ScalarAffineFunction{T}), - 0, - nothing, - UInt8[], - Box{T}(), + SingleVariableConstraints{T}(), C(), Dict{MOI.VariableIndex,String}(), nothing, diff --git a/src/Utilities/vector_bounds.jl b/src/Utilities/vector_bounds.jl new file mode 100644 index 0000000000..0d254a2d98 --- /dev/null +++ b/src/Utilities/vector_bounds.jl @@ -0,0 +1,410 @@ +abstract type AbstractVectorBounds end + +function set_from_constants( + b::AbstractVectorBounds, + ::Type{<:MOI.EqualTo}, + index, +) + return MOI.EqualTo(b.lower[index]) +end + +function set_from_constants( + b::AbstractVectorBounds, + S::Type{<:Union{MOI.GreaterThan,MOI.EqualTo}}, + index, +) + # Lower and upper bounds are equal for `EqualTo`, we can take either of them. + return S(b.lower[index]) +end + +function set_from_constants( + b::AbstractVectorBounds, + S::Type{<:MOI.LessThan}, + index, +) + return S(b.upper[index]) +end + +function set_from_constants( + b::AbstractVectorBounds, + S::Type{<:Union{MOI.Interval,MOI.Semicontinuous,MOI.Semiinteger}}, + index, +) + return S(b.lower[index], b.upper[index]) +end + +function set_from_constants( + ::AbstractVectorBounds, + S::Type{<:Union{MOI.Integer,MOI.ZeroOne}}, + index, +) + return S() +end + +""" + SUPPORTED_VARIABLE_SCALAR_SETS{T} + +The union of scalar sets for `SingleVariable` constraints supported by +`Utilities.Hyperrectangle` and `Utilities.SingleVariableConstraints`. +""" +const SUPPORTED_VARIABLE_SCALAR_SETS{T} = Union{ + MOI.EqualTo{T}, + MOI.GreaterThan{T}, + MOI.LessThan{T}, + MOI.Interval{T}, + MOI.Integer, + MOI.ZeroOne, + MOI.Semicontinuous{T}, + MOI.Semiinteger{T}, +} + +# 0xcb = 0x0080 | 0x0040 | 0x0008 | 0x0002 | 0x0001 +const _LOWER_BOUND_MASK = 0x00cb +# 0xcd = 0x0080 | 0x0040 | 0x0008 | 0x0004 | 0x0001 +const _UPPER_BOUND_MASK = 0x00cd + +const _DELETED_VARIABLE = 0x8000 + +_single_variable_flag(::Type{<:MOI.EqualTo}) = 0x0001 +_single_variable_flag(::Type{<:MOI.GreaterThan}) = 0x0002 +_single_variable_flag(::Type{<:MOI.LessThan}) = 0x0004 +_single_variable_flag(::Type{<:MOI.Interval}) = 0x0008 +_single_variable_flag(::Type{MOI.Integer}) = 0x0010 +_single_variable_flag(::Type{MOI.ZeroOne}) = 0x0020 +_single_variable_flag(::Type{<:MOI.Semicontinuous}) = 0x0040 +_single_variable_flag(::Type{<:MOI.Semiinteger}) = 0x0080 + +function _flag_to_set_type(flag::UInt16, ::Type{T}) where {T} + if flag == 0x0001 + return MOI.EqualTo{T} + elseif flag == 0x0002 + return MOI.GreaterThan{T} + elseif flag == 0x0004 + return MOI.LessThan{T} + elseif flag == 0x0008 + return MOI.Interval{T} + elseif flag == 0x0010 + return MOI.Integer + elseif flag == 0x0020 + return MOI.ZeroOne + elseif flag == 0x0040 + return MOI.Semicontinuous{T} + else + @assert flag == 0x0080 + return MOI.Semiinteger{T} + end +end + +# Julia doesn't infer `S1` correctly, so we use a function barrier to improve +# inference. +function _throw_if_lower_bound_set_inner(variable, S2, mask, T) + S1 = _flag_to_set_type(mask, T) + throw(MOI.LowerBoundAlreadySet{S1,S2}(variable)) + return +end + +function _throw_if_lower_bound_set(variable, S2, mask, T) + lower_mask = mask & _LOWER_BOUND_MASK + if iszero(lower_mask) + return # No lower bound set. + elseif iszero(_single_variable_flag(S2) & _LOWER_BOUND_MASK) + return # S2 isn't related to the lower bound. + end + return _throw_if_lower_bound_set_inner(variable, S2, lower_mask, T) +end + +# Julia doesn't infer `S1` correctly, so we use a function barrier to improve +# inference. +function _throw_if_upper_bound_set_inner(variable, S2, mask, T) + S1 = _flag_to_set_type(mask, T) + throw(MOI.UpperBoundAlreadySet{S1,S2}(variable)) + return +end + +function _throw_if_upper_bound_set(variable, S2, mask, T) + upper_mask = mask & _UPPER_BOUND_MASK + if iszero(upper_mask) + return # No upper bound set. + elseif iszero(_single_variable_flag(S2) & _UPPER_BOUND_MASK) + return # S2 isn't related to the upper bound. + end + return _throw_if_upper_bound_set_inner(variable, S2, upper_mask, T) +end + +function _lower_bound( + set::Union{MOI.GreaterThan,MOI.Interval,MOI.Semicontinuous,MOI.Semiinteger}, +) + return set.lower +end + +_lower_bound(set::MOI.EqualTo) = set.value + +function _upper_bound( + set::Union{MOI.LessThan,MOI.Interval,MOI.Semicontinuous,MOI.Semiinteger}, +) + return set.upper +end + +_upper_bound(set::MOI.EqualTo) = set.value + +# Use `-Inf` and `Inf` for `AbstractFloat` subtypes. +_no_lower_bound(::Type{T}) where {T} = zero(T) +_no_lower_bound(::Type{T}) where {T<:AbstractFloat} = typemin(T) +_no_upper_bound(::Type{T}) where {T} = zero(T) +_no_upper_bound(::Type{T}) where {T<:AbstractFloat} = typemax(T) + +### +### SingleVariableConstraints +### +### For use in MOI.Utilities.Model +### + +""" + struct SingleVariableConstraints{T} <: AbstractVectorBounds + set_mask::Vector{UInt16} + lower::Vector{T} + upper::Vector{T} + end + +A struct for storing SingleVariable-related constraints. Used in `MOI.Model`. +""" +mutable struct SingleVariableConstraints{T} <: AbstractVectorBounds + set_mask::Vector{UInt16} + lower::Vector{T} + upper::Vector{T} +end + +function SingleVariableConstraints{T}() where {T} + return SingleVariableConstraints{T}(UInt16[], T[], T[]) +end + +function MOI.throw_if_not_valid(b::SingleVariableConstraints, index) + if !MOI.is_valid(b, index) + throw(MOI.InvalidIndex(index)) + end +end + +function Base.:(==)(a::SingleVariableConstraints, b::SingleVariableConstraints) + return a.set_mask == b.set_mask && a.lower == b.lower && a.upper == b.upper +end + +function MOI.empty!(b::SingleVariableConstraints) + empty!(b.set_mask) + empty!(b.lower) + empty!(b.upper) + return b +end + +function MOI.is_empty(b::SingleVariableConstraints) + if length(b.set_mask) == 0 + return true + end + return all(isequal(_DELETED_VARIABLE), b.set_mask) +end + +function Base.resize!(b::SingleVariableConstraints, n) + resize!(b.set_mask, n) + resize!(b.lower, n) + resize!(b.upper, n) + return +end + +function MOI.add_variable(b::SingleVariableConstraints{T}) where {T} + push!(b.set_mask, 0x0000) + push!(b.lower, _no_lower_bound(T)) + push!(b.upper, _no_upper_bound(T)) + x = MOI.VariableIndex(length(b.set_mask)) + return x +end + +function MOI.get(b::SingleVariableConstraints, ::MOI.ListOfVariableIndices) + return MOI.VariableIndex[ + MOI.VariableIndex(i) for + i in 1:length(b.set_mask) if b.set_mask[i] != _DELETED_VARIABLE + ] +end + +function MOI.is_valid(b::SingleVariableConstraints, x::MOI.VariableIndex) + mask = get(b.set_mask, x.value, _DELETED_VARIABLE) + return mask != _DELETED_VARIABLE +end + +function MOI.get(b::SingleVariableConstraints, ::MOI.NumberOfVariables)::Int64 + if length(b.set_mask) == 0 + return 0 + end + return sum(x != _DELETED_VARIABLE for x in b.set_mask) +end + +function MOI.add_constraint( + b::SingleVariableConstraints{T}, + f::MOI.SingleVariable, + set::S, +) where {T,S} + flag = _single_variable_flag(S) + mask = b.set_mask[f.variable.value] + _throw_if_lower_bound_set(f.variable, S, mask, T) + _throw_if_upper_bound_set(f.variable, S, mask, T) + if !iszero(flag & _LOWER_BOUND_MASK) + b.lower[f.variable.value] = _lower_bound(set) + end + if !iszero(flag & _UPPER_BOUND_MASK) + b.upper[f.variable.value] = _upper_bound(set) + end + b.set_mask[f.variable.value] = mask | flag + return MOI.ConstraintIndex{MOI.SingleVariable,S}(f.variable.value) +end + +function MOI.delete( + b::SingleVariableConstraints{T}, + ci::MOI.ConstraintIndex{MOI.SingleVariable,S}, +) where {T,S} + MOI.throw_if_not_valid(b, ci) + flag = _single_variable_flag(S) + b.set_mask[ci.value] &= ~flag + if !iszero(flag & _LOWER_BOUND_MASK) + b.lower[ci.value] = _no_lower_bound(T) + end + if !iszero(flag & _UPPER_BOUND_MASK) + b.upper[ci.value] = _no_upper_bound(T) + end + return +end + +function MOI.delete(b::SingleVariableConstraints, x::MOI.VariableIndex) + MOI.throw_if_not_valid(b, x) + b.set_mask[x.value] = _DELETED_VARIABLE + return +end + +function MOI.is_valid( + b::SingleVariableConstraints, + ci::MOI.ConstraintIndex{MOI.SingleVariable,S}, +) where {S} + if !(1 <= ci.value <= length(b.set_mask)) + return false + end + return !iszero(b.set_mask[ci.value] & _single_variable_flag(S)) +end + +function MOI.set( + b::SingleVariableConstraints, + ::MOI.ConstraintSet, + ci::MOI.ConstraintIndex{MOI.SingleVariable,S}, + set::S, +) where {S} + flag = _single_variable_flag(S) + if !iszero(flag & _LOWER_BOUND_MASK) + b.lower[ci.value] = _lower_bound(set) + end + if !iszero(flag & _UPPER_BOUND_MASK) + b.upper[ci.value] = _upper_bound(set) + end + return +end + +function MOI.get( + b::SingleVariableConstraints, + ::MOI.NumberOfConstraints{MOI.SingleVariable,S}, +) where {S} + flag = _single_variable_flag(S) + return count(mask -> !iszero(flag & mask), b.set_mask) +end + +function _add_constraint_type( + list, + b::SingleVariableConstraints, + S::Type{<:MOI.AbstractScalarSet}, +) + flag = _single_variable_flag(S)::UInt16 + if any(mask -> !iszero(flag & mask), b.set_mask) + push!(list, (MOI.SingleVariable, S)) + end + return +end + +function MOI.get( + b::SingleVariableConstraints{T}, + ::MOI.ListOfConstraintTypesPresent, +) where {T} + list = Tuple{DataType,DataType}[] + _add_constraint_type(list, b, MOI.EqualTo{T}) + _add_constraint_type(list, b, MOI.GreaterThan{T}) + _add_constraint_type(list, b, MOI.LessThan{T}) + _add_constraint_type(list, b, MOI.Interval{T}) + _add_constraint_type(list, b, MOI.Semicontinuous{T}) + _add_constraint_type(list, b, MOI.Semiinteger{T}) + _add_constraint_type(list, b, MOI.Integer) + _add_constraint_type(list, b, MOI.ZeroOne) + return list +end + +function MOI.get( + b::SingleVariableConstraints, + ::MOI.ListOfConstraintIndices{MOI.SingleVariable,S}, +) where {S} + list = MOI.ConstraintIndex{MOI.SingleVariable,S}[] + flag = _single_variable_flag(S) + for (index, mask) in enumerate(b.set_mask) + if !iszero(mask & flag) + push!(list, MOI.ConstraintIndex{MOI.SingleVariable,S}(index)) + end + end + return list +end + +### +### Hyperrectangle +### + +""" + struct Hyperrectangle{T} <: AbstractVectorBounds + lower::Vector{T} + upper::Vector{T} + end + +A struct for the .constants field in MatrixOfConstraints. +""" +struct Hyperrectangle{T} <: AbstractVectorBounds + lower::Vector{T} + upper::Vector{T} +end + +Hyperrectangle{T}() where {T} = Hyperrectangle{T}(T[], T[]) + +function Base.:(==)(a::Hyperrectangle, b::Hyperrectangle) + return a.lower == b.lower && a.upper == b.upper +end + +function Base.empty!(b::Hyperrectangle) + empty!(b.lower) + empty!(b.upper) + return b +end + +function Base.resize!(b::Hyperrectangle, n) + resize!(b.lower, n) + resize!(b.upper, n) + return +end + +function load_constants( + b::Hyperrectangle{T}, + offset, + set::SUPPORTED_VARIABLE_SCALAR_SETS{T}, +) where {T} + flag = _single_variable_flag(typeof(set)) + if iszero(flag & _LOWER_BOUND_MASK) + b.lower[offset+1] = _no_lower_bound(T) + else + b.lower[offset+1] = _lower_bound(set) + end + if iszero(flag & _UPPER_BOUND_MASK) + b.upper[offset+1] = _no_upper_bound(T) + else + b.upper[offset+1] = _upper_bound(set) + end + return +end + +function_constants(::Hyperrectangle{T}, row) where {T} = zero(T) diff --git a/test/Utilities/matrix_of_constraints.jl b/test/Utilities/matrix_of_constraints.jl index 74f71a2fdf..30663b1ae8 100644 --- a/test/Utilities/matrix_of_constraints.jl +++ b/test/Utilities/matrix_of_constraints.jl @@ -192,18 +192,18 @@ function test_contlinear() end function test_contlinear(Indexing) A2 = sparse([1, 1], [1, 2], ones(2)) - b2 = MOI.Utilities.Box([-Inf], [1.0]) + b2 = MOI.Utilities.Hyperrectangle([-Inf], [1.0]) Alp = sparse( [1, 1, 2, 3, 4, 4], [1, 2, 1, 2, 1, 2], Float64[3, 2, 1, -1, 5, -4], ) - blp = MOI.Utilities.Box([5, 0, -Inf, 6], [5, Inf, 0, 7]) + blp = MOI.Utilities.Hyperrectangle([5, 0, -Inf, 6], [5, Inf, 0, 7]) F = MOI.ScalarAffineFunction{Float64} @testset "$SetType" for SetType in [MixLP{Float64}, OrdLP{Float64}] _test( MOIT.linear2test, - MOI.Utilities.Box{Float64}, + MOI.Utilities.Hyperrectangle{Float64}, SetType, A2, b2, @@ -223,7 +223,7 @@ function test_contlinear(Indexing) end _test( _lp, - MOI.Utilities.Box{Float64}, + MOI.Utilities.Hyperrectangle{Float64}, SetType, Alp, blp, @@ -363,7 +363,7 @@ end function test_get_by_name(T::Type, SetsType::Type) model = matrix_instance( T, - MOI.Utilities.Box{T}, + MOI.Utilities.Hyperrectangle{T}, SetsType, MOI.Utilities.OneBasedIndexing, ) @@ -393,7 +393,7 @@ MOIU.@struct_of_constraints_by_function_types( function test_nametest() T = Float64 Indexing = MOIU.OneBasedIndexing - ConstantsType = MOIU.Box{T} + ConstantsType = MOIU.Hyperrectangle{T} for ProductOfSetsType in [MixLP{Float64}, OrdLP{Float64}] model = MOIU.GenericOptimizer{ T, @@ -443,7 +443,7 @@ end function test_valid() T = Float64 Indexing = MOIU.OneBasedIndexing - ConstantsType = MOIU.Box{T} + ConstantsType = MOIU.Hyperrectangle{T} for ProductOfSetsType in [MixLP{Float64}, OrdLP{Float64}] model = matrix_instance(T, ConstantsType, ProductOfSetsType, Indexing) MOI.DeprecatedTest.validtest(model, delete = false) @@ -452,7 +452,7 @@ end function test_supports_constraint(T::Type = Float64, BadT::Type = Float32) Indexing = MOIU.OneBasedIndexing - ConstantsType = MOIU.Box{T} + ConstantsType = MOIU.Hyperrectangle{T} for ProductOfSetsType in [MixLP{Float64}, OrdLP{Float64}] model = MOIU.GenericOptimizer{ T, @@ -490,7 +490,7 @@ function test_copy(Indexing) MOIU.MatrixOfConstraints{ T, MOIU.MutableSparseMatrixCSC{T,Int,Indexing}, - MOIU.Box{T}, + MOIU.Hyperrectangle{T}, ScalarSetsType, }, MOIU.MatrixOfConstraints{ @@ -511,8 +511,12 @@ function test_copy() end function test_modif() - model = - matrix_instance(Int, MOIU.Box{Int}, OrdLP{Int}, MOIU.OneBasedIndexing) + model = matrix_instance( + Int, + MOIU.Hyperrectangle{Int}, + OrdLP{Int}, + MOIU.OneBasedIndexing, + ) x = MOI.add_variable(model) fx = MOI.SingleVariable(x) func = 2fx diff --git a/test/Utilities/model.jl b/test/Utilities/model.jl index eeb7532c0f..e92e0e56ce 100644 --- a/test/Utilities/model.jl +++ b/test/Utilities/model.jl @@ -162,14 +162,6 @@ function test_TestExternalModel() return end -function test_flag_to_set_type() - T = Int - @test_throws AssertionError MOI.Utilities.flag_to_set_type(0x11, T) - @test MOI.Utilities.flag_to_set_type(0x10, T) == MOI.Integer - @test MOI.Utilities.flag_to_set_type(0x20, T) == MOI.ZeroOne - return -end - function test_bound_twice() for T in [Int, Float64] model = MOI.Utilities.Model{T}() diff --git a/test/Utilities/vector_bounds.jl b/test/Utilities/vector_bounds.jl new file mode 100644 index 0000000000..5c2633274a --- /dev/null +++ b/test/Utilities/vector_bounds.jl @@ -0,0 +1,264 @@ +module TestBox + +using Test +import MathOptInterface +const MOI = MathOptInterface + +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$(name)", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end + return +end + +function test_empty() + a = MOI.Utilities.SingleVariableConstraints( + [0x0000, 0x0000], + [1, 2], + [3, 4], + ) + MOI.empty!(a) + @test MOI.is_empty(a) + return +end + +function test_resize() + a = MOI.Utilities.SingleVariableConstraints( + [0x0000, 0x0000], + [1, 2], + [3, 4], + ) + @test length(a.set_mask) == 2 + @test length(a.lower) == 2 + @test length(a.upper) == 2 + resize!(a, 4) + @test length(a.set_mask) == 4 + @test length(a.lower) == 4 + @test length(a.upper) == 4 + return +end + +function test_add_variable() + a = MOI.Utilities.SingleVariableConstraints{Int}() + MOI.add_variable(a) + @test a == MOI.Utilities.SingleVariableConstraints{Int}([0x0000], [0], [0]) + a = MOI.Utilities.SingleVariableConstraints{Float64}() + MOI.add_variable(a) + @test a == MOI.Utilities.SingleVariableConstraints{Float64}( + [0x0000], + [-Inf], + [Inf], + ) + return +end + +function test__flag_to_set_type() + T = Int + @test_throws AssertionError MOI.Utilities._flag_to_set_type(0x0011, T) + @test MOI.Utilities._flag_to_set_type(0x0010, T) == MOI.Integer + @test MOI.Utilities._flag_to_set_type(0x0020, T) == MOI.ZeroOne + return +end + +function test_add_constraint() + a = MOI.Utilities.SingleVariableConstraints{Int}() + x = MOI.add_variable(a) + @test a == + MOI.Utilities.SingleVariableConstraints{Int}([0x0000], Int[0], Int[0]) + f = MOI.SingleVariable(x) + MOI.add_constraint(a, f, MOI.GreaterThan(3)) + @test a == MOI.Utilities.SingleVariableConstraints{Int}([0x0002], [3], [0]) + MOI.add_constraint(a, f, MOI.LessThan(4)) + @test a == MOI.Utilities.SingleVariableConstraints{Int}([0x0006], [3], [4]) + return +end + +function test_add_constraint_LowerBoundAlreadySet() + a = MOI.Utilities.SingleVariableConstraints{Int}() + x = MOI.add_variable(a) + f = MOI.SingleVariable(x) + MOI.add_constraint(a, f, MOI.GreaterThan(3)) + @test_throws( + MOI.LowerBoundAlreadySet{MOI.GreaterThan{Int},MOI.GreaterThan{Int}}, + MOI.add_constraint(a, f, MOI.GreaterThan(3)), + ) + return +end + +function test_add_constraint_UpperBoundAlreadySet() + a = MOI.Utilities.SingleVariableConstraints{Int}() + x = MOI.add_variable(a) + f = MOI.SingleVariable(x) + MOI.add_constraint(a, f, MOI.LessThan(3)) + @test_throws( + MOI.UpperBoundAlreadySet{MOI.LessThan{Int},MOI.LessThan{Int}}, + MOI.add_constraint(a, f, MOI.LessThan(3)), + ) + return +end + +function test_delete_constraint_LessThan() + a = MOI.Utilities.SingleVariableConstraints{Int}() + x = MOI.add_variable(a) + f = MOI.SingleVariable(x) + c = MOI.add_constraint(a, f, MOI.LessThan(3)) + @test MOI.is_valid(a, c) + MOI.delete(a, c) + @test !MOI.is_valid(a, c) + return +end + +function test_delete_variable() + a = MOI.Utilities.SingleVariableConstraints{Int}() + x = MOI.add_variable(a) + @test a == + MOI.Utilities.SingleVariableConstraints{Int}([0x0000], Int[0], Int[0]) + f = MOI.SingleVariable(x) + c = MOI.add_constraint(a, f, MOI.LessThan(3)) + MOI.delete(a, x) + @test a == + MOI.Utilities.SingleVariableConstraints{Int}([0x8000], Int[0], Int[3]) + return +end + +function test_delete_constraint_GreaterThan() + a = MOI.Utilities.SingleVariableConstraints{Int}() + x = MOI.add_variable(a) + f = MOI.SingleVariable(x) + c = MOI.add_constraint(a, f, MOI.GreaterThan(3)) + @test MOI.is_valid(a, c) + MOI.delete(a, c) + @test !MOI.is_valid(a, c) + return +end + +function test_set_ConstraintSet() + a = MOI.Utilities.SingleVariableConstraints{Int}() + x = MOI.add_variable(a) + @test a == + MOI.Utilities.SingleVariableConstraints{Int}([0x0000], Int[0], Int[0]) + f = MOI.SingleVariable(x) + c = MOI.add_constraint(a, f, MOI.GreaterThan(3)) + @test a == + MOI.Utilities.SingleVariableConstraints{Int}([0x0002], Int[3], Int[0]) + MOI.set(a, MOI.ConstraintSet(), c, MOI.GreaterThan(2)) + @test a == + MOI.Utilities.SingleVariableConstraints{Int}([0x0002], Int[2], Int[0]) + return +end + +function test_NumberOfConstraints() + b = MOI.Utilities.SingleVariableConstraints( + [0x0008, 0x0002, 0x0004, 0x0006], + [1.0, 3.0, -Inf, -1.0], + [2.0, Inf, 4.0, 1.0], + ) + get(S) = MOI.get(b, MOI.NumberOfConstraints{MOI.SingleVariable,S}()) + @test get(MOI.ZeroOne) == 0 + @test get(MOI.GreaterThan{Float64}) == 2 + @test get(MOI.LessThan{Float64}) == 2 + @test get(MOI.Interval{Float64}) == 1 + return +end + +function test_ListOfConstraintTypesPresent() + b = MOI.Utilities.SingleVariableConstraints( + [0x0008, 0x0002, 0x0004, 0x0006], + [1.0, 3.0, -Inf, -1.0], + [2.0, Inf, 4.0, 1.0], + ) + @test MOI.get(b, MOI.ListOfConstraintTypesPresent()) == [ + (MOI.SingleVariable, MOI.GreaterThan{Float64}), + (MOI.SingleVariable, MOI.LessThan{Float64}), + (MOI.SingleVariable, MOI.Interval{Float64}), + ] + return +end + +function test_ListOfConstraintIndices() + b = MOI.Utilities.SingleVariableConstraints{Float64}( + [0x0008, 0x0002, 0x0004, 0x0006], + [1.0, 3.0, -Inf, -1.0], + [2.0, Inf, 4.0, 1.0], + ) + get(S) = MOI.get(b, MOI.ListOfConstraintIndices{MOI.SingleVariable,S}()) + @test get(MOI.ZeroOne) == [] + @test get(MOI.GreaterThan{Float64}) == [ + MOI.ConstraintIndex{MOI.SingleVariable,MOI.GreaterThan{Float64}}(2), + MOI.ConstraintIndex{MOI.SingleVariable,MOI.GreaterThan{Float64}}(4), + ] + @test get(MOI.LessThan{Float64}) == + MOI.ConstraintIndex{MOI.SingleVariable,MOI.LessThan{Float64}}.([3, 4]) + @test get(MOI.Interval{Float64}) == + [MOI.ConstraintIndex{MOI.SingleVariable,MOI.Interval{Float64}}(1)] + return +end + +### +### Hyperrectangle +### + +function test_Hyperrectangle_equal() + a = MOI.Utilities.Hyperrectangle([1, 2], [3, 4]) + b = MOI.Utilities.Hyperrectangle([1.0, 2.0], [3.0, 4.0]) + c = MOI.Utilities.Hyperrectangle([1.0, 3.0], [3.0, 4.0]) + d = MOI.Utilities.Hyperrectangle([1.0, 2.0], [3.0, 5.0]) + @test a == a + @test a == b + @test a != c + @test a != d + return +end + +function test_Hyperrectangle_empty() + a = MOI.Utilities.Hyperrectangle([1, 2], [3, 4]) + empty!(a) + @test a.lower == Int[] + @test a.upper == Int[] + return +end + +function test_Hyperrectangle_resize() + a = MOI.Utilities.Hyperrectangle([1, 2], [3, 4]) + @test length(a.lower) == 2 + @test length(a.upper) == 2 + resize!(a, 4) + @test length(a.lower) == 4 + @test length(a.upper) == 4 + return +end + +function test_load_constants() + a = MOI.Utilities.Hyperrectangle([-Inf, -Inf, -Inf], [Inf, Inf, Inf]) + MOI.Utilities.load_constants(a, 0, MOI.Interval(1.0, 2.0)) + MOI.Utilities.load_constants(a, 1, MOI.GreaterThan(3.0)) + MOI.Utilities.load_constants(a, 2, MOI.LessThan(4.0)) + @test a == MOI.Utilities.Hyperrectangle([1.0, 3.0, -Inf], [2.0, Inf, 4.0]) + return +end + +function test_function_constants() + a = MOI.Utilities.Hyperrectangle([-Inf, -Inf, -Inf], [Inf, Inf, Inf]) + MOI.Utilities.function_constants(a, 0) == 0.0 + return +end + +function test_set_from_constants() + a = MOI.Utilities.Hyperrectangle([1.0, 3.0, -Inf], [2.0, Inf, 4.0]) + @test MOI.Utilities.set_from_constants(a, MOI.Interval{Float64}, 1) == + MOI.Interval(1.0, 2.0) + @test MOI.Utilities.set_from_constants(a, MOI.GreaterThan{Float64}, 2) == + MOI.GreaterThan(3.0) + @test MOI.Utilities.set_from_constants(a, MOI.LessThan{Float64}, 3) == + MOI.LessThan(4.0) + @test MOI.Utilities.set_from_constants(a, MOI.ZeroOne, 2) == MOI.ZeroOne() + return +end + +end # module + +TestBox.runtests()