From d9791bfc82daa90a78832afc3fd28fff27b98d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 15 Jun 2021 18:33:05 -0400 Subject: [PATCH 1/5] Various improvements to MatrixOfConstraints --- src/Test/modellike.jl | 65 ++++++----- src/Utilities/Utilities.jl | 1 + src/Utilities/box.jl | 139 +++++++++++++++++++++++ src/Utilities/matrix_of_constraints.jl | 100 +++++++++-------- src/Utilities/model.jl | 115 +++---------------- src/Utilities/product_of_sets.jl | 9 +- src/Utilities/sparse_matrix.jl | 32 +++++- src/Utilities/struct_of_constraints.jl | 48 +++++++- src/Utilities/vector_of_constraints.jl | 1 + test/Utilities/matrix_of_constraints.jl | 140 ++++++++++++++++++++++++ 10 files changed, 460 insertions(+), 190 deletions(-) create mode 100644 src/Utilities/box.jl diff --git a/src/Test/modellike.jl b/src/Test/modellike.jl index 78ca2116b4..2a4d5200bc 100644 --- a/src/Test/modellike.jl +++ b/src/Test/modellike.jl @@ -25,7 +25,7 @@ function default_status_test(model::MOI.ModelLike) @test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION end -function nametest(model::MOI.ModelLike) +function nametest(model::MOI.ModelLike; delete::Bool=true) @testset "Variables" begin MOI.empty!(model) x = MOI.add_variables(model, 2) @@ -69,7 +69,7 @@ function nametest(model::MOI.ModelLike) MOI.set(model, MOI.ConstraintName(), c1, "c1") @test_throws ErrorException MOI.get(model, MOI.ConstraintIndex, "c1") end - @testset "Name test with $(typeof(model))" begin + @testset "Name" begin MOI.empty!(model) @test MOI.supports_incremental_interface(model, true) #=copy_names=# @test MOI.supports(model, MOI.Name()) @@ -218,25 +218,30 @@ function nametest(model::MOI.ModelLike) @test MOI.get(model, typeof(ca), nameb) === nothing end end - MOI.delete(model, v[2]) - @test MOI.get(model, MOI.VariableIndex, "Var2") === nothing - MOI.delete(model, c) - @test MOI.get(model, typeof(c), "Con1") === nothing - @test MOI.get(model, MOI.ConstraintIndex, "Con1") === nothing - MOI.delete(model, x) - @test MOI.get(model, MOI.VariableIndex, "Varx") === nothing - @test MOI.get(model, MOI.ConstraintIndex, "Con3") === nothing - @test MOI.get(model, typeof(c2), "Con2") === c2 - @test MOI.get(model, MOI.ConstraintIndex, "Con2") === c2 - MOI.delete(model, y) - @test MOI.get(model, typeof(cy), "Con4") === nothing - @test MOI.get(model, MOI.ConstraintIndex, "Con4") === nothing - for i in 1:4 - @test MOI.get(model, MOI.VariableIndex, "Vary$i") === nothing + if delete + MOI.delete(model, v[2]) + @test MOI.get(model, MOI.VariableIndex, "Var2") === nothing + + MOI.delete(model, c) + @test MOI.get(model, typeof(c), "Con1") === nothing + @test MOI.get(model, MOI.ConstraintIndex, "Con1") === nothing + + MOI.delete(model, x) + @test MOI.get(model, MOI.VariableIndex, "Varx") === nothing + @test MOI.get(model, MOI.ConstraintIndex, "Con3") === nothing + @test MOI.get(model, typeof(c2), "Con2") === c2 + @test MOI.get(model, MOI.ConstraintIndex, "Con2") === c2 + + MOI.delete(model, y) + @test MOI.get(model, typeof(cy), "Con4") === nothing + @test MOI.get(model, MOI.ConstraintIndex, "Con4") === nothing + for i in 1:4 + @test MOI.get(model, MOI.VariableIndex, "Vary$i") === nothing + end + MOI.set(model, MOI.ConstraintName(), c2, "Con4") + @test MOI.get(model, typeof(c2), "Con4") === c2 + @test MOI.get(model, MOI.ConstraintIndex, "Con4") === c2 end - MOI.set(model, MOI.ConstraintName(), c2, "Con4") - @test MOI.get(model, typeof(c2), "Con4") === c2 - @test MOI.get(model, MOI.ConstraintIndex, "Con4") === c2 end @testset "Duplicate names" begin @testset "Variables" begin @@ -252,8 +257,10 @@ function nametest(model::MOI.ModelLike) @test MOI.get(model, MOI.VariableIndex, "y") == y MOI.set(model, MOI.VariableName(), z, "x") @test_throws ErrorException MOI.get(model, MOI.VariableIndex, "x") - MOI.delete(model, x) - @test MOI.get(model, MOI.VariableIndex, "x") == z + if delete + MOI.delete(model, x) + @test MOI.get(model, MOI.VariableIndex, "x") == z + end end @testset "ScalarAffineFunction" begin MOI.empty!(model) @@ -272,14 +279,16 @@ function nametest(model::MOI.ModelLike) @test MOI.get(model, MOI.ConstraintIndex, "y") == c[2] MOI.set(model, MOI.ConstraintName(), c[3], "x") @test_throws ErrorException MOI.get(model, MOI.ConstraintIndex, "x") - MOI.delete(model, c[1]) - @test MOI.get(model, MOI.ConstraintIndex, "x") == c[3] + if delete + MOI.delete(model, c[1]) + @test MOI.get(model, MOI.ConstraintIndex, "x") == c[3] + end end end end # Taken from https://github.com/jump-dev/MathOptInterfaceUtilities.jl/issues/41 -function validtest(model::MOI.ModelLike) +function validtest(model::MOI.ModelLike; delete::Bool=true) MOI.empty!(model) @test MOI.supports_incremental_interface(model, false) #=copy_names=# v = MOI.add_variables(model, 2) @@ -287,8 +296,10 @@ function validtest(model::MOI.ModelLike) @test MOI.is_valid(model, v[2]) x = MOI.add_variable(model) @test MOI.is_valid(model, x) - MOI.delete(model, x) - @test !MOI.is_valid(model, x) + if delete + MOI.delete(model, x) + @test !MOI.is_valid(model, x) + end cf = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0, 1.0], v), 0.0) @test MOI.supports_constraint(model, typeof(cf), MOI.LessThan{Float64}) c = MOI.add_constraint(model, cf, MOI.LessThan(1.0)) diff --git a/src/Utilities/Utilities.jl b/src/Utilities/Utilities.jl index 69e47a3e38..ae55e56cb4 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("box.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 new file mode 100644 index 0000000000..d3f2bc4d20 --- /dev/null +++ b/src/Utilities/box.jl @@ -0,0 +1,139 @@ +# 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 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/matrix_of_constraints.jl b/src/Utilities/matrix_of_constraints.jl index 4da4e72fe3..055a1d2fff 100644 --- a/src/Utilities/matrix_of_constraints.jl +++ b/src/Utilities/matrix_of_constraints.jl @@ -5,6 +5,7 @@ sets::ST caches::Vector{Any} are_indices_mapped::Vector{BitSet} + final_touch::Bool end Represent `ScalarAffineFunction` and `VectorAffinefunction` constraints in a @@ -59,6 +60,7 @@ The `.constants::BT` type must implement: * `Base.empty!(::BT)` * `Base.resize(::BT)` * [`MOI.Utilities.load_constants`](@ref) + * [`MOI.Utilities.set_from_constants`](@ref) The `.sets::ST` type must implement: @@ -82,8 +84,11 @@ mutable struct MatrixOfConstraints{T,AT,BT,ST} <: MOI.ModelLike sets::ST caches::Vector{Any} are_indices_mapped::Vector{BitSet} + final_touch::Bool function MatrixOfConstraints{T,AT,BT,ST}() where {T,AT,BT,ST} - return new{T,AT,BT,ST}(AT(), BT(), ST(), Any[], BitSet[]) + model = new{T,AT,BT,ST}(AT(), BT(), ST(), Any[], BitSet[], false) + MOI.empty!(model) + return model end end @@ -168,6 +173,17 @@ The constants are loaded in three steps: """ function load_constants end +""" + set_from_constants(constants, S::Type, rows)::S + +This function returns an instance of the set `S` for which the constants where +loaded with [`load_constants`](@ref) at the rows `rows`. + +This function should be implemented to be usable as storage of constants for +[`MatrixOfConstraints`](@ref). +""" +function set_from_constants end + ### ### Interface for the .sets field ### @@ -227,6 +243,7 @@ function MOI.empty!(v::MatrixOfConstraints{T}) where {T} v.caches = [Tuple{_affine_function_type(T, S),S}[] for S in set_types(v.sets)] v.are_indices_mapped = [BitSet() for _ in eachindex(v.caches)] + v.final_touch = false return end @@ -263,8 +280,11 @@ function MOI.supports_constraint( return F == _affine_function_type(T, S) && set_index(v.sets, S) !== nothing end -function MOI.is_valid(v::MatrixOfConstraints, ci::MOI.ConstraintIndex) - return MOI.is_valid(v.sets, ci) +function MOI.is_valid( + v::MatrixOfConstraints{T}, + ci::MOI.ConstraintIndex{F,S}, +) where {T,F,S} + return F == _affine_function_type(T, S) && MOI.is_valid(v.sets, ci) end function MOI.get( @@ -382,6 +402,9 @@ function _load_constraints( end _add_variable(model::MatrixOfConstraints) = add_column(model.coefficients) +function _add_variables(model::MatrixOfConstraints, n) + return add_columns(model.coefficients, n) +end function pass_nonvariable_constraints( dest::MatrixOfConstraints, @@ -398,6 +421,10 @@ function pass_nonvariable_constraints( end function final_touch(model::MatrixOfConstraints, index_map) + if model.final_touch + @assert index_map === nothing + return + end final_touch(model.sets) num_rows = MOI.dimension(model.sets) resize!(model.constants, num_rows) @@ -410,6 +437,7 @@ function final_touch(model::MatrixOfConstraints, index_map) final_touch(model.coefficients) empty!(model.caches) empty!(model.are_indices_mapped) + model.final_touch = true return end @@ -446,56 +474,26 @@ function load_constants( copyto!(b, offset + 1, func.constants) return end +# FIXME does not work for all sets +set_from_constants(::Vector, ::Type{S}, rows) where {S} = S(length(rows)) -### -### .constants::Box -### - -""" - 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 +function MOI.get( + model::MatrixOfConstraints, + attr::Union{MOI.CanonicalConstraintFunction,MOI.ConstraintFunction}, + ci::MOI.ConstraintIndex, +) + @assert model.final_touch + MOI.throw_if_not_valid(model, ci) + return extract_function(model.coefficients, rows(model, ci)) end -function Base.resize!(b::Box, n) - resize!(b.lower, n) - resize!(b.upper, n) - return +function MOI.get( + model::MatrixOfConstraints, + attr::MOI.ConstraintSet, + ci::MOI.ConstraintIndex{F,S}, +) where {F,S} + @assert model.final_touch + MOI.throw_if_not_valid(model, ci) + return set_from_constants(model.constants, S, rows(model, ci)) end -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] = typemin(T) - else - b.lower[offset+1] = extract_lower_bound(set) - end - if iszero(flag & UPPER_BOUND_MASK) - b.upper[offset+1] = typemax(T) - else - b.upper[offset+1] = extract_upper_bound(set) - end - return -end diff --git a/src/Utilities/model.jl b/src/Utilities/model.jl index be7051dc45..30209366f5 100644 --- a/src/Utilities/model.jl +++ b/src/Utilities/model.jl @@ -14,12 +14,6 @@ function MOI.get(model::AbstractModel, ::MOI.NumberOfVariables)::Int64 end 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 _add_variable end @@ -30,11 +24,11 @@ variable has been added. This is similar to 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) - push!(model.lower_bound, _no_lower_bound(T)) - push!(model.upper_bound, _no_upper_bound(T)) + add_free(model.variable_bounds) if model.variable_indices !== nothing push!(model.variable_indices, vi) end @@ -481,36 +475,6 @@ function throw_if_upper_bound_set(variable, S2, mask, T) return _throw_if_upper_bound_set(variable, S2, upper_mask, T) end -# 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}, -} function MOI.supports_constraint( ::AbstractModel{T}, ::Type{MOI.SingleVariable}, @@ -537,12 +501,7 @@ function MOI.add_constraint( 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`. - if !iszero(flag & LOWER_BOUND_MASK) - model.lower_bound[index] = extract_lower_bound(s) - end - if !iszero(flag & UPPER_BOUND_MASK) - model.upper_bound[index] = extract_upper_bound(s) - end + merge_bounds(model.variable_bounds, index, s) model.single_variable_mask[index] = mask | flag return CI{MOI.SingleVariable,typeof(s)}(index) end @@ -571,10 +530,10 @@ function _delete_constraint( flag = single_variable_flag(S) model.single_variable_mask[ci.value] &= ~flag if !iszero(flag & LOWER_BOUND_MASK) - model.lower_bound[ci.value] = _no_lower_bound(T) + model.variable_bounds.lower[ci.value] = _no_lower_bound(T) end if !iszero(flag & UPPER_BOUND_MASK) - model.upper_bound[ci.value] = _no_upper_bound(T) + model.variable_bounds.upper[ci.value] = _no_upper_bound(T) end return end @@ -614,13 +573,7 @@ function MOI.set( set::S, ) where {T,S<:SUPPORTED_VARIABLE_SCALAR_SETS{T}} MOI.throw_if_not_valid(model, ci) - flag = single_variable_flag(typeof(set)) - if !iszero(flag & LOWER_BOUND_MASK) - model.lower_bound[ci.value] = extract_lower_bound(set) - end - if !iszero(flag & UPPER_BOUND_MASK) - model.upper_bound[ci.value] = extract_upper_bound(set) - end + merge_bounds(model.variable_bounds, ci.value, set) return end @@ -722,49 +675,13 @@ function MOI.get( return MOI.get(model.constraints, attr, ci) end -function _get_single_variable_set( - model::AbstractModel, - ::Type{<:MOI.EqualTo}, - index, -) - return MOI.EqualTo(model.lower_bound[index]) -end -function _get_single_variable_set( - model::AbstractModel, - S::Type{<:Union{MOI.GreaterThan,MOI.EqualTo}}, - index, -) - # Lower and upper bounds are equal for `EqualTo`, we can take either of them. - return S(model.lower_bound[index]) -end -function _get_single_variable_set( - model::AbstractModel, - S::Type{<:MOI.LessThan}, - index, -) - return S(model.upper_bound[index]) -end -function _get_single_variable_set( - model::AbstractModel, - S::Type{<:Union{MOI.Interval,MOI.Semicontinuous,MOI.Semiinteger}}, - index, -) - return S(model.lower_bound[index], model.upper_bound[index]) -end -function _get_single_variable_set( - ::AbstractModel, - S::Type{<:Union{MOI.Integer,MOI.ZeroOne}}, - index, -) - return S() -end function MOI.get( model::AbstractModel, ::MOI.ConstraintSet, ci::CI{MOI.SingleVariable,S}, ) where {S} MOI.throw_if_not_valid(model, ci) - return _get_single_variable_set(model, S, ci.value) + return set_from_constants(model.variable_bounds, S, ci.value) end function MOI.is_empty(model::AbstractModel) @@ -785,8 +702,7 @@ function MOI.empty!(model::AbstractModel{T}) where {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.variable_bounds) empty!(model.var_to_name) model.name_to_var = nothing empty!(model.con_to_name) @@ -1057,8 +973,8 @@ for (loop_name, loop_super_type) in [ * `F`-in-`S` constraints that are supported by `C`. The lower (resp. upper) bound of a variable of index `VariableIndex(i)` - is at the `i`th index of the vector stored in the field `lower_bound` - (resp. `upper_bound`). When no lower (resp. upper) bound is set, it is + is at the `i`th index of the vector stored in the field `variable_bounds.lower` + (resp. `variable_bounds.upper`). When no lower (resp. upper) bound is set, it is `typemin(T)` (resp. `typemax(T)`) if `T <: AbstractFloat`. """ mutable struct $name{T,C} <: $super_type{T} @@ -1078,12 +994,8 @@ for (loop_name, loop_super_type) in [ # 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} - # Lower bound set by `SingleVariable`-in-`S` where `S`is - # `GreaterThan{T}`, `EqualTo{T}` or `Interval{T}`. - lower_bound::Vector{T} - # Lower bound set by `SingleVariable`-in-`S` where `S`is - # `LessThan{T}`, `EqualTo{T}` or `Interval{T}`. - upper_bound::Vector{T} + # Bounds set by `SingleVariable`-in-`S`: + variable_bounds::Box{T} constraints::C var_to_name::Dict{MOI.VariableIndex,String} # If `nothing`, the dictionary hasn't been constructed yet. @@ -1103,8 +1015,7 @@ for (loop_name, loop_super_type) in [ 0, nothing, UInt8[], - T[], - T[], + Box{T}(), C(), Dict{MOI.VariableIndex,String}(), nothing, diff --git a/src/Utilities/product_of_sets.jl b/src/Utilities/product_of_sets.jl index 7cebdc10b1..b2f5f642fa 100644 --- a/src/Utilities/product_of_sets.jl +++ b/src/Utilities/product_of_sets.jl @@ -246,10 +246,9 @@ function final_touch(sets::OrderedProductOfSets) end function _num_rows(sets::OrderedProductOfSets, ::Type{S}) where {S} - @assert sets.final_touch i = set_index(sets, S) - if i == 1 - return sets.num_rows[1] + if !sets.final_touch || i == 1 + return sets.num_rows[i] end return sets.num_rows[i] - sets.num_rows[i-1] end @@ -258,7 +257,6 @@ function MOI.get( sets::OrderedProductOfSets{T}, ::MOI.ListOfConstraintTypesPresent, ) where {T} - @assert sets.final_touch return Tuple{DataType,DataType}[ (_affine_function_type(T, S), S) for S in set_types(sets) if _num_rows(sets, S) > 0 @@ -325,7 +323,6 @@ function MOI.get( sets::OrderedProductOfSets, ::MOI.NumberOfConstraints{F,S}, ) where {F,S} - @assert sets.final_touch r = _range_iterator(sets, F, S) return _length(r) end @@ -334,7 +331,6 @@ function MOI.get( sets::OrderedProductOfSets, ::MOI.ListOfConstraintIndices{F,S}, ) where {F,S} - @assert sets.final_touch rows = _range_iterator(sets, F, S) if rows === nothing return MOI.ConstraintIndex{F,S}[] @@ -346,7 +342,6 @@ function MOI.is_valid( sets::OrderedProductOfSets, ci::MOI.ConstraintIndex{F,S}, ) where {F,S} - @assert sets.final_touch r = _range_iterator(sets, F, S) return r !== nothing && ci.value in r end diff --git a/src/Utilities/sparse_matrix.jl b/src/Utilities/sparse_matrix.jl index a4d058aff5..0a5cc36c29 100644 --- a/src/Utilities/sparse_matrix.jl +++ b/src/Utilities/sparse_matrix.jl @@ -66,7 +66,7 @@ mutable struct MutableSparseMatrixCSC{Tv,Ti<:Integer,I<:AbstractIndexing} rowval::Vector{Ti} nzval::Vector{Tv} function MutableSparseMatrixCSC{Tv,Ti,I}() where {Tv,Ti<:Integer,I} - return new{Tv,Ti,I}(I(), 0, 0, Ti[], Ti[], Tv[]) + return new{Tv,Ti,I}(I(), 0, 0, [zero(Ti)], Ti[], Tv[]) end end @@ -85,6 +85,14 @@ function add_column(A::MutableSparseMatrixCSC{Tv,Ti}) where {Tv,Ti} push!(A.colptr, zero(Ti)) return end +function add_columns(A::MutableSparseMatrixCSC{Tv,Ti}, n) where {Tv,Ti} + A.n += n + for _ in 1:n + push!(A.colptr, zero(Ti)) + end + return +end + function set_number_of_rows(A::MutableSparseMatrixCSC, num_rows) A.m = num_rows @@ -159,3 +167,25 @@ function Base.convert( A.nzval, ) end + +function extract_function( + A::MutableSparseMatrixCSC{T}, + row::Integer, +) where {T} + func = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{T}[], zero(T)) + for col in 1:A.n + range = A.colptr[col]:(A.colptr[col+1]-1) + idx = searchsortedfirst(range, row, by = i -> A.rowval[i]) + if idx <= length(range) && A.rowval[range[idx]] == row + push!(func.terms, MOI.ScalarAffineTerm(A.nzval[range[idx]], MOI.VariableIndex(col))) + end + end + return func +end +#function extract_function( +# A::MutableSparseMatrixCSC{T}, +# rows::AbstractVector{<:Integer}, +# func = MOI.VectorAffineFunction(MOI.VectorAffineTerm{T}[], zero(T)) +#) where {T} +# return func +#end diff --git a/src/Utilities/struct_of_constraints.jl b/src/Utilities/struct_of_constraints.jl index 619f551d0e..d5dc2c8b0f 100644 --- a/src/Utilities/struct_of_constraints.jl +++ b/src/Utilities/struct_of_constraints.jl @@ -1,8 +1,19 @@ abstract type StructOfConstraints <: MOI.ModelLike end function _add_variable(model::StructOfConstraints) + model.num_variables += 1 return broadcastcall(_add_variable, model) end +function _add_variables(model::StructOfConstraints, n) + model.num_variables += n + return broadcastcall(Base.Fix2(_add_variables, n), model) +end + +function final_touch(::Nothing, index_map) end +function final_touch(model::StructOfConstraints, index_map) + broadcastcall(Base.Fix2(final_touch, index_map), model) + return +end function _throw_if_cannot_delete(model::StructOfConstraints, vis, fast_in_vis) broadcastcall(model) do constrs @@ -130,6 +141,7 @@ function MOI.is_empty(model::StructOfConstraints) end function MOI.empty!(model::StructOfConstraints) + model.num_variables = 0 broadcastcall(model) do constrs if constrs !== nothing MOI.empty!(constrs) @@ -176,11 +188,31 @@ struct SymbolFun <: SymbolFS s::Union{Symbol,Expr} typed::Bool end +SymbolFun(s::Symbol) = SymbolFun(s, false) +function SymbolFun(s::Expr) + if Meta.isexpr(s, :curly) + @assert length(s.args) == 2 + @assert s.args[2] == :T + return SymbolFun(s.args[1], true) + else + return SymbolFun(s, false) + end +end struct SymbolSet <: SymbolFS s::Union{Symbol,Expr} typed::Bool end +SymbolSet(s::Symbol) = SymbolSet(s, false) +function SymbolSet(s::Expr) + if Meta.isexpr(s, :curly) + @assert length(s.args) == 2 + @assert s.args[2] == :T + return SymbolSet(s.args[1], true) + else + return SymbolSet(s, false) + end +end _typed(s::SymbolFS) = s.typed ? Expr(:curly, esc(s.s), esc(:T)) : esc(s.s) @@ -216,7 +248,9 @@ function struct_of_constraint_code(struct_name, types, field_types = nothing) append!(typed_struct.args, field_types) end code = quote - mutable struct $typed_struct <: StructOfConstraints end + mutable struct $typed_struct <: StructOfConstraints + num_variables::Int64 + end function $MOIU.broadcastcall(f::Function, model::$struct_name) $(Expr(:block, _callfield.(Ref(:f), types)...)) @@ -250,6 +284,7 @@ function struct_of_constraint_code(struct_name, types, field_types = nothing) )::$(field_type) where {$T} if model.$field === nothing model.$field = $(field_type)() + $MOI.Utilities._add_variables(model.$field, model.num_variables) end return model.$field end @@ -278,7 +313,7 @@ function struct_of_constraint_code(struct_name, types, field_types = nothing) # If there is no field type, the default constructor is sufficient and # adding this constructor will make a `StackOverflow`. constructor_code = :(function $typed_struct() where {$T} - return $typed_struct($([:(nothing) for _ in field_types]...)) + return $typed_struct(0, $([:(nothing) for _ in field_types]...)) end) if type_parametrized append!(constructor_code.args[1].args, field_types) @@ -287,3 +322,12 @@ function struct_of_constraint_code(struct_name, types, field_types = nothing) end return code end + +macro struct_of_constraints_by_function_types(name, func_types...) + funcs = SymbolFun.(func_types) + return struct_of_constraint_code(esc(name), funcs) +end +macro struct_of_constraints_by_set_types(name, set_types...) + sets = SymbolSet.(set_types) + return struct_of_constraint_code(esc(name), sets) +end diff --git a/src/Utilities/vector_of_constraints.jl b/src/Utilities/vector_of_constraints.jl index 286a0bbfb6..2af9c7d517 100644 --- a/src/Utilities/vector_of_constraints.jl +++ b/src/Utilities/vector_of_constraints.jl @@ -146,6 +146,7 @@ function MOI.modify( end function _add_variable(::VectorOfConstraints) end +function _add_variables(::VectorOfConstraints, ::Int64) end # Deletion of variables in vector of variables diff --git a/test/Utilities/matrix_of_constraints.jl b/test/Utilities/matrix_of_constraints.jl index cb83a2b063..ec14c769dd 100644 --- a/test/Utilities/matrix_of_constraints.jl +++ b/test/Utilities/matrix_of_constraints.jl @@ -370,3 +370,143 @@ end @testset "Get constraint by name" begin test_get_by_name() end + +MOIU.@struct_of_constraints_by_function_types( + VoVorSAff, + MOI.VectorOfVariables, + MOI.ScalarAffineFunction{T}, +) + +function test_nametest() + T = Float64 + Indexing = MOIU.OneBasedIndexing + ConstantsType = MOIU.Box{T} + for ProductOfSetsType in [MixLP{Float64}, OrdLP{Float64}] + model = MOIU.GenericOptimizer{ + T, + VoVorSAff{T}{ + MOIU.VectorOfConstraints{MOI.VectorOfVariables,MOI.Nonpositives}, + MOIU.MatrixOfConstraints{ + T, + MOIU.MutableSparseMatrixCSC{T,Int,Indexing}, + ConstantsType, + ProductOfSetsType, + }, + }, + }() + MOI.Test.nametest( + model, + delete = false, + ) + end +end + +@testset "test_nametest" begin + test_nametest() +end + + +MOIU.@struct_of_constraints_by_function_types( + VoVorVAff, + MOI.VectorOfVariables, + MOI.VectorAffineFunction{T}, +) + +MOIU.@product_of_sets(Zeros, MOI.Zeros) + +function test_empty() + T = Float64 + Indexing = MOIU.OneBasedIndexing + model = MOIU.GenericOptimizer{ + T, + VoVorVAff{T}{ + MOIU.VectorOfConstraints{MOI.VectorOfVariables,MOI.Nonnegatives}, + MOIU.MatrixOfConstraints{ + T, + MOIU.MutableSparseMatrixCSC{T,Int,Indexing}, + Vector{T}, + Zeros{Float64}, + }, + }, + }() + MOI.Test.emptytest(model) +end + +@testset "test_empty" begin + test_empty() +end + +function test_valid() + T = Float64 + Indexing = MOIU.OneBasedIndexing + ConstantsType = MOIU.Box{T} + for ProductOfSetsType in [MixLP{Float64}, OrdLP{Float64}] + model = matrix_instance(T, ConstantsType, ProductOfSetsType, Indexing) + MOI.Test.validtest(model, delete = false) + end +end + +@testset "test_valid" begin + test_valid() +end + +function test_supports_constraint(T::Type=Float64, BadT::Type=Float32) + Indexing = MOIU.OneBasedIndexing + ConstantsType = MOIU.Box{T} + for ProductOfSetsType in [MixLP{Float64}, OrdLP{Float64}] + model = MOIU.GenericOptimizer{ + T, + VoVorSAff{T}{ + MOIU.VectorOfConstraints{MOI.VectorOfVariables,MOI.Zeros}, + MOIU.MatrixOfConstraints{ + T, + MOIU.MutableSparseMatrixCSC{T,Int,Indexing}, + ConstantsType, + ProductOfSetsType, + }, + }, + }() + MOI.Test.supports_constrainttest(model, T, BadT) + end +end + +@testset "test_supports_constraint" begin + test_supports_constraint() +end + +MOIU.@struct_of_constraints_by_function_types( + VoVorSAfforVAff, + MOI.VectorOfVariables, + MOI.ScalarAffineFunction{T}, + MOI.VectorAffineFunction{T}, +) + +function test_copy() + T = Float64 + Indexing = MOIU.OneBasedIndexing + for ScalarSetsType in [MixLP{T}, OrdLP{T}] + model = MOIU.GenericOptimizer{ + T, + VoVorSAfforVAff{T}{ + MOIU.VectorOfConstraints{MOI.VectorOfVariables,MOI.Nonnegatives}, + MOIU.MatrixOfConstraints{ + T, + MOIU.MutableSparseMatrixCSC{T,Int,Indexing}, + MOIU.Box{T}, + ScalarSetsType, + }, + MOIU.MatrixOfConstraints{ + T, + MOIU.MutableSparseMatrixCSC{T,Int,Indexing}, + Vector{T}, + Zeros{T}, + }, + }, + }() + MOI.Test.copytest(model, MOIU.Model{T}()) + end +end + +@testset "test_copy" begin + test_copy() +end From ca19bbe4193722825f282eb27dcc3b16ebdb08b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Wed, 16 Jun 2021 23:50:33 -0400 Subject: [PATCH 2/5] Fix tests --- src/Utilities/box.jl | 2 + src/Utilities/matrix_of_constraints.jl | 11 +++- src/Utilities/sparse_matrix.jl | 79 +++++++++++++++++++------ src/Utilities/struct_of_constraints.jl | 16 ++--- test/Utilities/matrix_of_constraints.jl | 6 +- 5 files changed, 82 insertions(+), 32 deletions(-) diff --git a/src/Utilities/box.jl b/src/Utilities/box.jl index d3f2bc4d20..30ed57cfbf 100644 --- a/src/Utilities/box.jl +++ b/src/Utilities/box.jl @@ -101,6 +101,8 @@ function merge_bounds(b::Box, index, set) end end +function add_constants_to_functions(::Box, index, ::MOI.ScalarAffineFunction) end + function set_from_constants( b::Box, ::Type{<:MOI.EqualTo}, diff --git a/src/Utilities/matrix_of_constraints.jl b/src/Utilities/matrix_of_constraints.jl index 055a1d2fff..f245050899 100644 --- a/src/Utilities/matrix_of_constraints.jl +++ b/src/Utilities/matrix_of_constraints.jl @@ -61,6 +61,7 @@ The `.constants::BT` type must implement: * `Base.resize(::BT)` * [`MOI.Utilities.load_constants`](@ref) * [`MOI.Utilities.set_from_constants`](@ref) + * [`MOI.Utilities.add_constants_to_functions`](@ref) The `.sets::ST` type must implement: @@ -474,6 +475,11 @@ function load_constants( copyto!(b, offset + 1, func.constants) return end +function add_constants_to_functions(b::Vector, rows, func::MOI.VectorAffineFunction) + for (i, row) in enumerate(rows) + func.constants[i] = b[row] + end +end # FIXME does not work for all sets set_from_constants(::Vector, ::Type{S}, rows) where {S} = S(length(rows)) @@ -484,7 +490,10 @@ function MOI.get( ) @assert model.final_touch MOI.throw_if_not_valid(model, ci) - return extract_function(model.coefficients, rows(model, ci)) + r = rows(model, ci) + func = extract_function(model.coefficients, r) + add_constants_to_functions(model.constants, r, func) + return func end function MOI.get( diff --git a/src/Utilities/sparse_matrix.jl b/src/Utilities/sparse_matrix.jl index 0a5cc36c29..320d5b5698 100644 --- a/src/Utilities/sparse_matrix.jl +++ b/src/Utilities/sparse_matrix.jl @@ -70,6 +70,19 @@ mutable struct MutableSparseMatrixCSC{Tv,Ti<:Integer,I<:AbstractIndexing} end end +function SparseArrays.nzrange( + A::MutableSparseMatrixCSC{Tv,Ti,ZeroBasedIndexing}, + col, +) where {Tv,Ti} + return (A.colptr[col] + 1):A.colptr[col + 1] +end +function SparseArrays.nzrange( + A::MutableSparseMatrixCSC{Tv,Ti,OneBasedIndexing}, + col, +) where {Tv,Ti} + return A.colptr[col]:(A.colptr[col + 1] - 1) +end + function MOI.empty!(A::MutableSparseMatrixCSC{Tv,Ti}) where {Tv,Ti} A.m = 0 A.n = 0 @@ -93,11 +106,10 @@ function add_columns(A::MutableSparseMatrixCSC{Tv,Ti}, n) where {Tv,Ti} return end - function set_number_of_rows(A::MutableSparseMatrixCSC, num_rows) A.m = num_rows for i in 3:length(A.colptr) - A.colptr[i] += A.colptr[i-1] + A.colptr[i] += A.colptr[i - 1] end resize!(A.rowval, A.colptr[end]) resize!(A.nzval, A.colptr[end]) @@ -106,7 +118,7 @@ end function final_touch(A::MutableSparseMatrixCSC) for i in length(A.colptr):-1:2 - A.colptr[i] = _shift(A.colptr[i-1], ZeroBasedIndexing(), A.indexing) + A.colptr[i] = _shift(A.colptr[i - 1], ZeroBasedIndexing(), A.indexing) end A.colptr[1] = _first_index(A.indexing) return @@ -121,7 +133,7 @@ function allocate_terms( func::Union{MOI.ScalarAffineFunction,MOI.VectorAffineFunction}, ) for term in func.terms - A.colptr[index_map[_variable(term)].value+1] += 1 + A.colptr[index_map[_variable(term)].value + 1] += 1 end return end @@ -168,24 +180,55 @@ function Base.convert( ) end +function _first_in_column( + A::MutableSparseMatrixCSC{Tv,Ti}, + row::Integer, + col::Integer, +) where {Tv,Ti} + range = SparseArrays.nzrange(A, col) + idx = searchsortedfirst(view(A.rowval, range), row) + return get(range, idx, A.colptr[col + 1]) +end +function extract_function(A::MutableSparseMatrixCSC{T}, row::Integer) where {T} + func = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{T}[], zero(T)) + for col in 1:(A.n) + idx = _first_in_column(A, row, col) + if idx < A.colptr[col + 1] && A.rowval[idx] == row + push!( + func.terms, + MOI.ScalarAffineTerm(A.nzval[idx], MOI.VariableIndex(col)), + ) + end + end + return func +end function extract_function( A::MutableSparseMatrixCSC{T}, - row::Integer, + rows::UnitRange, ) where {T} - func = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{T}[], zero(T)) - for col in 1:A.n - range = A.colptr[col]:(A.colptr[col+1]-1) - idx = searchsortedfirst(range, row, by = i -> A.rowval[i]) - if idx <= length(range) && A.rowval[range[idx]] == row - push!(func.terms, MOI.ScalarAffineTerm(A.nzval[range[idx]], MOI.VariableIndex(col))) + func = MOI.VectorAffineFunction( + MOI.VectorAffineTerm{T}[], + zeros(T, length(rows)), + ) + isempty(rows) && return func + idx = [_first_in_column(A, first(rows), col) for col in 1:(A.n)] + for output_index in eachindex(rows) + for col in 1:(A.n) + idx[col] < A.colptr[col + 1] || continue + row = A.rowval[idx[col]] + row == rows[output_index] || continue + push!( + func.terms, + MOI.VectorAffineTerm( + output_index, + MOI.ScalarAffineTerm( + A.nzval[idx[col]], + MOI.VariableIndex(col), + ), + ), + ) + idx[col] += 1 end end return func end -#function extract_function( -# A::MutableSparseMatrixCSC{T}, -# rows::AbstractVector{<:Integer}, -# func = MOI.VectorAffineFunction(MOI.VectorAffineTerm{T}[], zero(T)) -#) where {T} -# return func -#end diff --git a/src/Utilities/struct_of_constraints.jl b/src/Utilities/struct_of_constraints.jl index d5dc2c8b0f..53d17e6edc 100644 --- a/src/Utilities/struct_of_constraints.jl +++ b/src/Utilities/struct_of_constraints.jl @@ -309,17 +309,13 @@ function struct_of_constraint_code(struct_name, types, field_types = nothing) end ), ) - if !isempty(field_types) - # If there is no field type, the default constructor is sufficient and - # adding this constructor will make a `StackOverflow`. - constructor_code = :(function $typed_struct() where {$T} - return $typed_struct(0, $([:(nothing) for _ in field_types]...)) - end) - if type_parametrized - append!(constructor_code.args[1].args, field_types) - end - push!(code.args, constructor_code) + constructor_code = :(function $typed_struct() where {$T} + return $typed_struct(0, $([:(nothing) for _ in field_types]...)) + end) + if type_parametrized + append!(constructor_code.args[1].args, field_types) end + push!(code.args, constructor_code) return code end diff --git a/test/Utilities/matrix_of_constraints.jl b/test/Utilities/matrix_of_constraints.jl index ec14c769dd..3b83cf292f 100644 --- a/test/Utilities/matrix_of_constraints.jl +++ b/test/Utilities/matrix_of_constraints.jl @@ -481,9 +481,8 @@ MOIU.@struct_of_constraints_by_function_types( MOI.VectorAffineFunction{T}, ) -function test_copy() +function test_copy(Indexing) T = Float64 - Indexing = MOIU.OneBasedIndexing for ScalarSetsType in [MixLP{T}, OrdLP{T}] model = MOIU.GenericOptimizer{ T, @@ -508,5 +507,6 @@ function test_copy() end @testset "test_copy" begin - test_copy() + test_copy(MOIU.ZeroBasedIndexing) + test_copy(MOIU.OneBasedIndexing) end From b3a235175de7ec9447b200ae3495c5e4e895e004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Mon, 21 Jun 2021 17:25:24 -0400 Subject: [PATCH 3/5] Fixes --- docs/src/submodules/Utilities/reference.md | 2 + src/Test/modellike.jl | 4 +- src/Utilities/box.jl | 15 +--- src/Utilities/matrix_of_constraints.jl | 18 +++-- src/Utilities/sparse_matrix.jl | 46 ++++++------ src/Utilities/struct_of_constraints.jl | 5 +- test/Utilities/matrix_of_constraints.jl | 81 +++++++++++----------- test/Utilities/model.jl | 60 ++++++++-------- 8 files changed, 113 insertions(+), 118 deletions(-) diff --git a/docs/src/submodules/Utilities/reference.md b/docs/src/submodules/Utilities/reference.md index 253f58e2bf..3c248ed8fc 100644 --- a/docs/src/submodules/Utilities/reference.md +++ b/docs/src/submodules/Utilities/reference.md @@ -106,6 +106,8 @@ Utilities.OneBasedIndexing ```@docs Utilities.load_constants +Utilities.set_from_constants +Utilities.constants ``` ```@docs diff --git a/src/Test/modellike.jl b/src/Test/modellike.jl index 2a4d5200bc..f617171bcb 100644 --- a/src/Test/modellike.jl +++ b/src/Test/modellike.jl @@ -25,7 +25,7 @@ function default_status_test(model::MOI.ModelLike) @test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION end -function nametest(model::MOI.ModelLike; delete::Bool=true) +function nametest(model::MOI.ModelLike; delete::Bool = true) @testset "Variables" begin MOI.empty!(model) x = MOI.add_variables(model, 2) @@ -288,7 +288,7 @@ function nametest(model::MOI.ModelLike; delete::Bool=true) end # Taken from https://github.com/jump-dev/MathOptInterfaceUtilities.jl/issues/41 -function validtest(model::MOI.ModelLike; delete::Bool=true) +function validtest(model::MOI.ModelLike; delete::Bool = true) MOI.empty!(model) @test MOI.supports_incremental_interface(model, false) #=copy_names=# v = MOI.add_variables(model, 2) diff --git a/src/Utilities/box.jl b/src/Utilities/box.jl index 30ed57cfbf..6f961dcc33 100644 --- a/src/Utilities/box.jl +++ b/src/Utilities/box.jl @@ -29,7 +29,6 @@ const SUPPORTED_VARIABLE_SCALAR_SETS{T} = Union{ MOI.Semiinteger{T}, } - """ struct Box{T} lower::Vector{T} @@ -101,13 +100,9 @@ function merge_bounds(b::Box, index, set) end end -function add_constants_to_functions(::Box, index, ::MOI.ScalarAffineFunction) end +constants(::Box{T}, row) where {T} = zero(T) -function set_from_constants( - b::Box, - ::Type{<:MOI.EqualTo}, - index, -) +function set_from_constants(b::Box, ::Type{<:MOI.EqualTo}, index) return MOI.EqualTo(b.lower[index]) end function set_from_constants( @@ -118,11 +113,7 @@ function set_from_constants( # 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, -) +function set_from_constants(b::Box, S::Type{<:MOI.LessThan}, index) return S(b.upper[index]) end function set_from_constants( diff --git a/src/Utilities/matrix_of_constraints.jl b/src/Utilities/matrix_of_constraints.jl index f245050899..dedcf06b4d 100644 --- a/src/Utilities/matrix_of_constraints.jl +++ b/src/Utilities/matrix_of_constraints.jl @@ -61,7 +61,7 @@ The `.constants::BT` type must implement: * `Base.resize(::BT)` * [`MOI.Utilities.load_constants`](@ref) * [`MOI.Utilities.set_from_constants`](@ref) - * [`MOI.Utilities.add_constants_to_functions`](@ref) + * [`MOI.Utilities.constants`](@ref) The `.sets::ST` type must implement: @@ -475,11 +475,8 @@ function load_constants( copyto!(b, offset + 1, func.constants) return end -function add_constants_to_functions(b::Vector, rows, func::MOI.VectorAffineFunction) - for (i, row) in enumerate(rows) - func.constants[i] = b[row] - end -end +constants(b::Vector, rows) = b[rows] + # FIXME does not work for all sets set_from_constants(::Vector, ::Type{S}, rows) where {S} = S(length(rows)) @@ -491,9 +488,11 @@ function MOI.get( @assert model.final_touch MOI.throw_if_not_valid(model, ci) r = rows(model, ci) - func = extract_function(model.coefficients, r) - add_constants_to_functions(model.constants, r, func) - return func + return extract_function( + model.coefficients, + r, + constants(model.constants, r), + ) end function MOI.get( @@ -505,4 +504,3 @@ function MOI.get( MOI.throw_if_not_valid(model, ci) return set_from_constants(model.constants, S, rows(model, ci)) end - diff --git a/src/Utilities/sparse_matrix.jl b/src/Utilities/sparse_matrix.jl index 320d5b5698..79f8ce7377 100644 --- a/src/Utilities/sparse_matrix.jl +++ b/src/Utilities/sparse_matrix.jl @@ -70,17 +70,10 @@ mutable struct MutableSparseMatrixCSC{Tv,Ti<:Integer,I<:AbstractIndexing} end end -function SparseArrays.nzrange( - A::MutableSparseMatrixCSC{Tv,Ti,ZeroBasedIndexing}, - col, -) where {Tv,Ti} - return (A.colptr[col] + 1):A.colptr[col + 1] -end -function SparseArrays.nzrange( - A::MutableSparseMatrixCSC{Tv,Ti,OneBasedIndexing}, - col, -) where {Tv,Ti} - return A.colptr[col]:(A.colptr[col + 1] - 1) +function SparseArrays.nzrange(A::MutableSparseMatrixCSC, col) + start = _shift(A.colptr[col], A.indexing, OneBasedIndexing()) + stop = _shift(A.colptr[col+1], A.indexing, ZeroBasedIndexing()) + return start:stop end function MOI.empty!(A::MutableSparseMatrixCSC{Tv,Ti}) where {Tv,Ti} @@ -109,7 +102,7 @@ end function set_number_of_rows(A::MutableSparseMatrixCSC, num_rows) A.m = num_rows for i in 3:length(A.colptr) - A.colptr[i] += A.colptr[i - 1] + A.colptr[i] += A.colptr[i-1] end resize!(A.rowval, A.colptr[end]) resize!(A.nzval, A.colptr[end]) @@ -118,7 +111,7 @@ end function final_touch(A::MutableSparseMatrixCSC) for i in length(A.colptr):-1:2 - A.colptr[i] = _shift(A.colptr[i - 1], ZeroBasedIndexing(), A.indexing) + A.colptr[i] = _shift(A.colptr[i-1], ZeroBasedIndexing(), A.indexing) end A.colptr[1] = _first_index(A.indexing) return @@ -133,7 +126,7 @@ function allocate_terms( func::Union{MOI.ScalarAffineFunction,MOI.VectorAffineFunction}, ) for term in func.terms - A.colptr[index_map[_variable(term)].value + 1] += 1 + A.colptr[index_map[_variable(term)].value+1] += 1 end return end @@ -186,14 +179,21 @@ function _first_in_column( col::Integer, ) where {Tv,Ti} range = SparseArrays.nzrange(A, col) + row = _shift(row, OneBasedIndexing(), A.indexing) idx = searchsortedfirst(view(A.rowval, range), row) - return get(range, idx, A.colptr[col + 1]) + return get(range, idx, last(range) + 1) end -function extract_function(A::MutableSparseMatrixCSC{T}, row::Integer) where {T} - func = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{T}[], zero(T)) +function extract_function( + A::MutableSparseMatrixCSC{T}, + row::Integer, + constant::T, +) where {T} + func = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{T}[], constant) for col in 1:(A.n) idx = _first_in_column(A, row, col) - if idx < A.colptr[col + 1] && A.rowval[idx] == row + idx <= last(SparseArrays.nzrange(A, col)) || continue + r = _shift(A.rowval[idx], A.indexing, OneBasedIndexing()) + if r == row push!( func.terms, MOI.ScalarAffineTerm(A.nzval[idx], MOI.VariableIndex(col)), @@ -205,17 +205,15 @@ end function extract_function( A::MutableSparseMatrixCSC{T}, rows::UnitRange, + constants::Vector{T}, ) where {T} - func = MOI.VectorAffineFunction( - MOI.VectorAffineTerm{T}[], - zeros(T, length(rows)), - ) + func = MOI.VectorAffineFunction(MOI.VectorAffineTerm{T}[], constants) isempty(rows) && return func idx = [_first_in_column(A, first(rows), col) for col in 1:(A.n)] for output_index in eachindex(rows) for col in 1:(A.n) - idx[col] < A.colptr[col + 1] || continue - row = A.rowval[idx[col]] + idx[col] <= last(SparseArrays.nzrange(A, col)) || continue + row = _shift(A.rowval[idx[col]], A.indexing, OneBasedIndexing()) row == rows[output_index] || continue push!( func.terms, diff --git a/src/Utilities/struct_of_constraints.jl b/src/Utilities/struct_of_constraints.jl index 53d17e6edc..ef1a69f314 100644 --- a/src/Utilities/struct_of_constraints.jl +++ b/src/Utilities/struct_of_constraints.jl @@ -284,7 +284,10 @@ function struct_of_constraint_code(struct_name, types, field_types = nothing) )::$(field_type) where {$T} if model.$field === nothing model.$field = $(field_type)() - $MOI.Utilities._add_variables(model.$field, model.num_variables) + $MOI.Utilities._add_variables( + model.$field, + model.num_variables, + ) end return model.$field end diff --git a/test/Utilities/matrix_of_constraints.jl b/test/Utilities/matrix_of_constraints.jl index 3b83cf292f..81882c011e 100644 --- a/test/Utilities/matrix_of_constraints.jl +++ b/test/Utilities/matrix_of_constraints.jl @@ -1,9 +1,23 @@ +module TestMatrixOfConstraints + using SparseArrays, Test + import MathOptInterface const MOI = MathOptInterface const MOIT = MOI.Test const MOIU = MOI.Utilities +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_matrix_equal(A::SparseMatrixCSC, B::SparseMatrixCSC) @test A.m == B.m @test A.n == B.n @@ -172,10 +186,11 @@ MOIU.@product_of_sets( MOI.Interval{T}, ) -@testset "contlinear $Indexing" for Indexing in [ - MOIU.OneBasedIndexing, - MOIU.ZeroBasedIndexing, -] +function test_contlinear() + test_contlinear(MOIU.OneBasedIndexing) + return test_contlinear(MOIU.ZeroBasedIndexing) +end +function test_contlinear(Indexing) A2 = sparse([1, 1], [1, 2], ones(2)) b2 = MOI.Utilities.Box([-Inf], [1.0]) Alp = sparse( @@ -253,10 +268,12 @@ MOIU.@product_of_sets(Nonneg, MOI.Nonnegatives) MOIU.@product_of_sets(NonposNonneg, MOI.Nonpositives, MOI.Nonnegatives) MOIU.@product_of_sets(NonnegNonpos, MOI.Nonnegatives, MOI.Nonpositives) -@testset "contconic $Indexing" for Indexing in [ - MOIU.OneBasedIndexing, - MOIU.ZeroBasedIndexing, -] +function test_contconic() + test_contconic(MOIU.OneBasedIndexing) + return test_contconic(MOIU.ZeroBasedIndexing) +end + +function test_contconic(Indexing) function _lin3_query(optimizer, con_types) @test con_types == MOI.get(optimizer, MOI.ListOfConstraintTypesPresent()) @@ -367,10 +384,6 @@ function test_get_by_name() end end -@testset "Get constraint by name" begin - test_get_by_name() -end - MOIU.@struct_of_constraints_by_function_types( VoVorSAff, MOI.VectorOfVariables, @@ -385,7 +398,10 @@ function test_nametest() model = MOIU.GenericOptimizer{ T, VoVorSAff{T}{ - MOIU.VectorOfConstraints{MOI.VectorOfVariables,MOI.Nonpositives}, + MOIU.VectorOfConstraints{ + MOI.VectorOfVariables, + MOI.Nonpositives, + }, MOIU.MatrixOfConstraints{ T, MOIU.MutableSparseMatrixCSC{T,Int,Indexing}, @@ -394,18 +410,10 @@ function test_nametest() }, }, }() - MOI.Test.nametest( - model, - delete = false, - ) + MOI.Test.nametest(model, delete = false) end end -@testset "test_nametest" begin - test_nametest() -end - - MOIU.@struct_of_constraints_by_function_types( VoVorVAff, MOI.VectorOfVariables, @@ -429,11 +437,7 @@ function test_empty() }, }, }() - MOI.Test.emptytest(model) -end - -@testset "test_empty" begin - test_empty() + return MOI.Test.emptytest(model) end function test_valid() @@ -446,11 +450,7 @@ function test_valid() end end -@testset "test_valid" begin - test_valid() -end - -function test_supports_constraint(T::Type=Float64, BadT::Type=Float32) +function test_supports_constraint(T::Type = Float64, BadT::Type = Float32) Indexing = MOIU.OneBasedIndexing ConstantsType = MOIU.Box{T} for ProductOfSetsType in [MixLP{Float64}, OrdLP{Float64}] @@ -470,10 +470,6 @@ function test_supports_constraint(T::Type=Float64, BadT::Type=Float32) end end -@testset "test_supports_constraint" begin - test_supports_constraint() -end - MOIU.@struct_of_constraints_by_function_types( VoVorSAfforVAff, MOI.VectorOfVariables, @@ -487,7 +483,10 @@ function test_copy(Indexing) model = MOIU.GenericOptimizer{ T, VoVorSAfforVAff{T}{ - MOIU.VectorOfConstraints{MOI.VectorOfVariables,MOI.Nonnegatives}, + MOIU.VectorOfConstraints{ + MOI.VectorOfVariables, + MOI.Nonnegatives, + }, MOIU.MatrixOfConstraints{ T, MOIU.MutableSparseMatrixCSC{T,Int,Indexing}, @@ -506,7 +505,11 @@ function test_copy(Indexing) end end -@testset "test_copy" begin +function test_copy() test_copy(MOIU.ZeroBasedIndexing) - test_copy(MOIU.OneBasedIndexing) + return test_copy(MOIU.OneBasedIndexing) end + +end + +TestMatrixOfConstraints.runtests() diff --git a/test/Utilities/model.jl b/test/Utilities/model.jl index d1f733b893..04b8cf5dea 100644 --- a/test/Utilities/model.jl +++ b/test/Utilities/model.jl @@ -6,52 +6,52 @@ const MOIU = MOI.Utilities function bound_vectors_test(::Type{T}, nolb, noub) where {T} model = MOIU.Model{T}() - @test model.lower_bound == T[] - @test model.upper_bound == T[] + @test model.variable_bounds.lower == T[] + @test model.variable_bounds.upper == T[] x = MOI.add_variable(model) fx = MOI.SingleVariable(x) - @test model.lower_bound == [nolb] - @test model.upper_bound == [noub] + @test model.variable_bounds.lower == [nolb] + @test model.variable_bounds.upper == [noub] ux = MOI.add_constraint(model, fx, MOI.LessThan(T(1))) - @test model.lower_bound == [nolb] - @test model.upper_bound == [T(1)] + @test model.variable_bounds.lower == [nolb] + @test model.variable_bounds.upper == [T(1)] y = MOI.add_variable(model) fy = MOI.SingleVariable(y) - @test model.lower_bound == [nolb, nolb] - @test model.upper_bound == [T(1), noub] + @test model.variable_bounds.lower == [nolb, nolb] + @test model.variable_bounds.upper == [T(1), noub] cy = MOI.add_constraint(model, fy, MOI.Interval(T(2), T(3))) - @test model.lower_bound == [nolb, T(2)] - @test model.upper_bound == [T(1), T(3)] + @test model.variable_bounds.lower == [nolb, T(2)] + @test model.variable_bounds.upper == [T(1), T(3)] lx = MOI.add_constraint(model, fx, MOI.GreaterThan(T(0))) - @test model.lower_bound == [T(0), T(2)] - @test model.upper_bound == [T(1), T(3)] + @test model.variable_bounds.lower == [T(0), T(2)] + @test model.variable_bounds.upper == [T(1), T(3)] MOI.delete(model, lx) - @test model.lower_bound == [nolb, T(2)] - @test model.upper_bound == [T(1), T(3)] + @test model.variable_bounds.lower == [nolb, T(2)] + @test model.variable_bounds.upper == [T(1), T(3)] MOI.delete(model, ux) - @test model.lower_bound == [nolb, T(2)] - @test model.upper_bound == [noub, T(3)] + @test model.variable_bounds.lower == [nolb, T(2)] + @test model.variable_bounds.upper == [noub, T(3)] cx = MOI.add_constraint(model, fx, MOI.Semicontinuous(T(3), T(4))) - @test model.lower_bound == [T(3), T(2)] - @test model.upper_bound == [T(4), T(3)] + @test model.variable_bounds.lower == [T(3), T(2)] + @test model.variable_bounds.upper == [T(4), T(3)] MOI.delete(model, cy) - @test model.lower_bound == [T(3), nolb] - @test model.upper_bound == [T(4), noub] + @test model.variable_bounds.lower == [T(3), nolb] + @test model.variable_bounds.upper == [T(4), noub] sy = MOI.add_constraint(model, fy, MOI.Semiinteger(T(-2), T(-1))) - @test model.lower_bound == [T(3), T(-2)] - @test model.upper_bound == [T(4), T(-1)] + @test model.variable_bounds.lower == [T(3), T(-2)] + @test model.variable_bounds.upper == [T(4), T(-1)] MOI.delete(model, sy) - @test model.lower_bound == [T(3), nolb] - @test model.upper_bound == [T(4), noub] + @test model.variable_bounds.lower == [T(3), nolb] + @test model.variable_bounds.upper == [T(4), noub] ey = MOI.add_constraint(model, fy, MOI.EqualTo(T(-3))) - @test model.lower_bound == [T(3), T(-3)] - @test model.upper_bound == [T(4), T(-3)] + @test model.variable_bounds.lower == [T(3), T(-3)] + @test model.variable_bounds.upper == [T(4), T(-3)] MOI.delete(model, ey) - @test model.lower_bound == [T(3), nolb] - @test model.upper_bound == [T(4), noub] + @test model.variable_bounds.lower == [T(3), nolb] + @test model.variable_bounds.upper == [T(4), noub] MOI.delete(model, cx) - @test model.lower_bound == [nolb, nolb] - @test model.upper_bound == [noub, noub] + @test model.variable_bounds.lower == [nolb, nolb] + @test model.variable_bounds.upper == [noub, noub] end @testset "Basic constraint tests" begin From 823fa05322255ca33dfa21cb7727786482650f7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Mon, 21 Jun 2021 17:36:28 -0400 Subject: [PATCH 4/5] Add docstrings --- docs/src/submodules/Utilities/reference.md | 2 ++ src/Utilities/struct_of_constraints.jl | 31 +++++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/docs/src/submodules/Utilities/reference.md b/docs/src/submodules/Utilities/reference.md index 3c248ed8fc..608c25f2e9 100644 --- a/docs/src/submodules/Utilities/reference.md +++ b/docs/src/submodules/Utilities/reference.md @@ -33,6 +33,8 @@ Utilities.UniversalFallback Utilities.@model Utilities.GenericModel Utilities.GenericOptimizer +Utilities.@struct_of_constraints_by_function_types +Utilities.@struct_of_constraints_by_set_types Utilities.struct_of_constraint_code ``` diff --git a/src/Utilities/struct_of_constraints.jl b/src/Utilities/struct_of_constraints.jl index ef1a69f314..4390e199e9 100644 --- a/src/Utilities/struct_of_constraints.jl +++ b/src/Utilities/struct_of_constraints.jl @@ -230,14 +230,18 @@ _mapreduce_field(s::SymbolFS) = :(cur = op(cur, f(model.$(_field(s))))) """ struct_of_constraint_code(struct_name, types, field_types = nothing) -Given a vector of `n` `SymbolFun` or `SymbolSet` in `types`, creates a -struct of name `struct_name` that is a subtype of -`StructOfConstraint{T, C1, C2, ..., Cn}` if `field_types` is `nothing` and -a subtype of `StructOfConstraint{T}` otherwise. +Given a vector of `n` `SymbolFun` or `SymbolSet` in `types`, defines +a subtype of `StructOfConstraints` of name `name` and which type parameters +`{T, F1, F2, ..., Fn}` if `field_types` is `nothing` and +a `{T}` otherwise. It contains `n` field where the `i`th field has type `Ci` if `field_types` is `nothing` and type `field_types[i]` otherwise. If `types` is vector of `SymbolFun` (resp. `SymbolSet`) then the constraints of that function (resp. set) type are stored in the corresponding field. + +This function is used by the macros [`@model`](@ref), +[`@struct_of_constraints_by_function_types`](@ref) and +[`@struct_of_constraints_by_set_types`](@ref). """ function struct_of_constraint_code(struct_name, types, field_types = nothing) T = esc(:T) @@ -322,10 +326,29 @@ function struct_of_constraint_code(struct_name, types, field_types = nothing) return code end +""" + Utilities.@struct_of_constraints_by_function_types(name, func_types...) + +Given a vector of `n` function types `(F1, F2,..., Fn)` in `func_types`, defines +a subtype of `StructOfConstraints` of name `name` and which type parameters +`{T, C1, C2, ..., Cn}`. +It contains `n` field where the `i`th field has type `Ci` and stores the +constraints of function type `Fi`. +""" macro struct_of_constraints_by_function_types(name, func_types...) funcs = SymbolFun.(func_types) return struct_of_constraint_code(esc(name), funcs) end + +""" + Utilities.@struct_of_constraints_by_set_types(name, func_types...) + +Given a vector of `n` set types `(S1, S2,..., Sn)` in `func_types`, defines +a subtype of `StructOfConstraints` of name `name` and which type parameters +`{T, C1, C2, ..., Cn}`. +It contains `n` field where the `i`th field has type `Ci` and stores the +constraints of set type `Si`. +""" macro struct_of_constraints_by_set_types(name, set_types...) sets = SymbolSet.(set_types) return struct_of_constraint_code(esc(name), sets) From 8ac2587daf1ca58755cadc3351977959156ef049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Mon, 21 Jun 2021 17:57:18 -0400 Subject: [PATCH 5/5] constants -> function_constants --- docs/src/submodules/Utilities/reference.md | 2 +- src/Utilities/box.jl | 2 +- src/Utilities/matrix_of_constraints.jl | 17 ++++++++++++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/src/submodules/Utilities/reference.md b/docs/src/submodules/Utilities/reference.md index 608c25f2e9..dc5fc69737 100644 --- a/docs/src/submodules/Utilities/reference.md +++ b/docs/src/submodules/Utilities/reference.md @@ -108,8 +108,8 @@ Utilities.OneBasedIndexing ```@docs Utilities.load_constants +Utilities.function_constants Utilities.set_from_constants -Utilities.constants ``` ```@docs diff --git a/src/Utilities/box.jl b/src/Utilities/box.jl index 6f961dcc33..b5915a05d4 100644 --- a/src/Utilities/box.jl +++ b/src/Utilities/box.jl @@ -100,7 +100,7 @@ function merge_bounds(b::Box, index, set) end end -constants(::Box{T}, row) where {T} = zero(T) +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]) diff --git a/src/Utilities/matrix_of_constraints.jl b/src/Utilities/matrix_of_constraints.jl index dedcf06b4d..60d9f25970 100644 --- a/src/Utilities/matrix_of_constraints.jl +++ b/src/Utilities/matrix_of_constraints.jl @@ -60,8 +60,8 @@ The `.constants::BT` type must implement: * `Base.empty!(::BT)` * `Base.resize(::BT)` * [`MOI.Utilities.load_constants`](@ref) + * [`MOI.Utilities.function_constants`](@ref) * [`MOI.Utilities.set_from_constants`](@ref) - * [`MOI.Utilities.constants`](@ref) The `.sets::ST` type must implement: @@ -174,6 +174,17 @@ The constants are loaded in three steps: """ function load_constants end +""" + function_constants(constants, rows) + +This function returns the function constants that were loaded with +[`load_constants`](@ref) at the rows `rows`. + +This function should be implemented to be usable as storage of constants for +[`MatrixOfConstraints`](@ref). +""" +function function_constants end + """ set_from_constants(constants, S::Type, rows)::S @@ -475,7 +486,7 @@ function load_constants( copyto!(b, offset + 1, func.constants) return end -constants(b::Vector, rows) = b[rows] +function_constants(b::Vector, rows) = b[rows] # FIXME does not work for all sets set_from_constants(::Vector, ::Type{S}, rows) where {S} = S(length(rows)) @@ -491,7 +502,7 @@ function MOI.get( return extract_function( model.coefficients, r, - constants(model.constants, r), + function_constants(model.constants, r), ) end