diff --git a/docs/src/dev.md b/docs/src/dev.md index 2f253dbe..337182ad 100644 --- a/docs/src/dev.md +++ b/docs/src/dev.md @@ -58,7 +58,6 @@ SparseMatrixColorings.valid_dynamic_order ```@docs SparseMatrixColorings.respectful_similar SparseMatrixColorings.matrix_versions -SparseMatrixColorings.same_pattern ``` ## Visualization diff --git a/src/adtypes.jl b/src/adtypes.jl index 7500b1e2..47fae343 100644 --- a/src/adtypes.jl +++ b/src/adtypes.jl @@ -6,7 +6,7 @@ function coloring( ) bg = BipartiteGraph(A) color = convert(Vector{eltype(bg)}, ADTypes.column_coloring(A, algo)) - return ColumnColoringResult(A, bg, color) + return ColumnColoringResult(A, bg, color; allow_denser=false) end function coloring( @@ -17,5 +17,5 @@ function coloring( ) bg = BipartiteGraph(A) color = convert(Vector{eltype(bg)}, ADTypes.row_coloring(A, algo)) - return RowColoringResult(A, bg, color) + return RowColoringResult(A, bg, color; allow_denser=false) end diff --git a/src/constant.jl b/src/constant.jl index d79caab0..ffb582b1 100644 --- a/src/constant.jl +++ b/src/constant.jl @@ -9,12 +9,22 @@ Indeed, for symmetric coloring problems, we need more than just the vector of co # Constructors - ConstantColoringAlgorithm{partition}(matrix_template, color) - ConstantColoringAlgorithm(matrix_template, color; partition=:column) - + ConstantColoringAlgorithm{partition}( + matrix_template, + color; + allow_denser=false + ) + + ConstantColoringAlgorithm( + matrix_template, color; + partition=:column, + allow_denser=false + ) + - `partition::Symbol`: either `:row` or `:column`. - `matrix_template::AbstractMatrix`: matrix for which the vector of colors was precomputed (the algorithm will only accept matrices of the exact same size). - `color::Vector{<:Integer}`: vector of integer colors, one for each row or column (depending on `partition`). +- `allow_denser::Bool`: whether or not to allow decompression into a `SparseMatrixCSC` which has more nonzeros than the original sparsity pattern (see [`decompress!`](@ref) to know when this is implemented). !!! warning The second constructor (based on keyword arguments) is type-unstable. @@ -73,30 +83,38 @@ struct ConstantColoringAlgorithm{ matrix_template::M color::Vector{T} result::R + allow_denser::Bool end function ConstantColoringAlgorithm{:column}( - matrix_template::AbstractMatrix, color::Vector{<:Integer} + matrix_template::AbstractMatrix, color::Vector{<:Integer}; allow_denser::Bool=false ) bg = BipartiteGraph(matrix_template) - result = ColumnColoringResult(matrix_template, bg, color) + result = ColumnColoringResult(matrix_template, bg, color; allow_denser) T, M, R = eltype(bg), typeof(matrix_template), typeof(result) - return ConstantColoringAlgorithm{:column,M,T,R}(matrix_template, color, result) + return ConstantColoringAlgorithm{:column,M,T,R}( + matrix_template, color, result, allow_denser + ) end function ConstantColoringAlgorithm{:row}( - matrix_template::AbstractMatrix, color::Vector{<:Integer} + matrix_template::AbstractMatrix, color::Vector{<:Integer}; allow_denser::Bool=false ) bg = BipartiteGraph(matrix_template) - result = RowColoringResult(matrix_template, bg, color) + result = RowColoringResult(matrix_template, bg, color; allow_denser) T, M, R = eltype(bg), typeof(matrix_template), typeof(result) - return ConstantColoringAlgorithm{:row,M,T,R}(matrix_template, color, result) + return ConstantColoringAlgorithm{:row,M,T,R}( + matrix_template, color, result, allow_denser + ) end function ConstantColoringAlgorithm( - matrix_template::AbstractMatrix, color::Vector{<:Integer}; partition::Symbol=:column + matrix_template::AbstractMatrix, + color::Vector{<:Integer}; + partition::Symbol=:column, + allow_denser::Bool=false, ) - return ConstantColoringAlgorithm{partition}(matrix_template, color) + return ConstantColoringAlgorithm{partition}(matrix_template, color; allow_denser) end function coloring( diff --git a/src/decompression.jl b/src/decompression.jl index 64e3c517..9f7e4200 100644 --- a/src/decompression.jl +++ b/src/decompression.jl @@ -193,9 +193,21 @@ The out-of-place alternative is [`decompress`](@ref). Compression means summing either the columns or the rows of `A` which share the same color. It is done by calling [`compress`](@ref). +# Details + For `:symmetric` coloring results (and for those only), an optional positional argument `uplo in (:U, :L, :F)` can be passed to specify which part of the matrix `A` should be updated: the Upper triangle, the Lower triangle, or the Full matrix. When `A isa SparseMatrixCSC`, using the `uplo` argument requires a target matrix which only stores the relevant triangle(s). +Some coloring algorithms ([`GreedyColoringAlgorithm`](@ref) and [`ConstantColoringAlgorithm`](@ref)) have an option called `allow_denser`, which enables in-place decompression into a `SparseMatrixCSC` containing more structural nonzeros than the sparsity pattern used for coloring. + +!!! warning + Decompression into a denser `SparseMatrixCSC` is only implemented when all the conditions below are satisfied simultaneously: + - the partition is `:row` or `:column` (not `:bidirectional`) + - the decompression is `:direct` (not `:substitution`) + - the decompression is for all colors (so [`decompress_single_color!`](@ref) will not work) + + Outside of these cases, it is up to the user to make sure that the sparsity pattern of the decompression target is an exact match. + # Example ```jldoctest @@ -210,6 +222,8 @@ julia> A = sparse([ julia> result = coloring(A, ColoringProblem(), GreedyColoringAlgorithm()); +julia> result_tolerant = coloring(A, ColoringProblem(), GreedyColoringAlgorithm(; allow_denser=true)); + julia> collect.(column_groups(result)) 3-element Vector{Vector{Int64}}: [1, 2, 4] @@ -225,6 +239,8 @@ julia> B = compress(A, result) julia> A2 = similar(A); +julia> A3 = similar(A); A3[1, 1] = A3[end, end] = 1; + julia> decompress!(A2, B, result) 4×6 SparseMatrixCSC{Int64, Int64} with 9 stored entries: ⋅ ⋅ 4 6 ⋅ 9 @@ -234,6 +250,16 @@ julia> decompress!(A2, B, result) julia> A2 == A true + +julia> decompress!(A3, B, result_tolerant) +4×6 SparseMatrixCSC{Int64, Int64} with 11 stored entries: + 0 ⋅ 4 6 ⋅ 9 + 1 ⋅ ⋅ ⋅ 7 ⋅ + ⋅ 2 ⋅ ⋅ 8 ⋅ + ⋅ 3 5 ⋅ ⋅ 0 + +julia> A3 == A +true ``` # See also @@ -258,6 +284,8 @@ Decompress the vector `b` corresponding to color `c` in-place into `A`, given a !!! warning This function will only update some coefficients of `A`, without resetting the rest to zero. +# Details + For `:symmetric` coloring results (and for those only), an optional positional argument `uplo in (:U, :L, :F)` can be passed to specify which part of the matrix `A` should be updated: the Upper triangle, the Lower triangle, or the Full matrix. When `A isa SparseMatrixCSC`, using the `uplo` argument requires a target matrix which only stores the relevant triangle(s). @@ -324,7 +352,7 @@ end function decompress!(A::AbstractMatrix, B::AbstractMatrix, result::ColumnColoringResult) (; color) = result S = result.bg.S2 - check_same_pattern(A, S) + @assert size(A) == size(S) fill!(A, zero(eltype(A))) rvS = rowvals(S) for j in axes(S, 2) @@ -342,7 +370,7 @@ function decompress_single_color!( ) (; group) = result S = result.bg.S2 - check_same_pattern(A, S) + @assert size(A) == size(S) rvS = rowvals(S) for j in group[c] for k in nzrange(S, j) @@ -354,12 +382,28 @@ function decompress_single_color!( end function decompress!(A::SparseMatrixCSC, B::AbstractMatrix, result::ColumnColoringResult) - (; compressed_indices) = result + (; compressed_indices, allow_denser) = result S = result.bg.S2 - check_same_pattern(A, S) + check_compatible_pattern(A, S; allow_denser) nzA = nonzeros(A) - for k in eachindex(nzA, compressed_indices) - nzA[k] = B[compressed_indices[k]] + if nnz(A) == nnz(S) + for k in eachindex(compressed_indices) + nzA[k] = B[compressed_indices[k]] + end + else # nnz(A) > nnz(S) + rvA, rvS = rowvals(A), rowvals(S) + shift = 0 + for j in axes(S, 2) + for k in nzrange(S, j) + i = rvS[k] + while (k + shift) < A.colptr[j] || rvA[k + shift] < i + nzA[k + shift] = zero(eltype(A)) + shift += 1 + end + nzA[k + shift] = B[compressed_indices[k]] + end + end + nzA[(nnz(S) + shift + 1):end] .= zero(eltype(A)) end return A end @@ -369,7 +413,7 @@ function decompress_single_color!( ) (; group) = result S = result.bg.S2 - check_same_pattern(A, S) + check_compatible_pattern(A, S) rvS = rowvals(S) nzA = nonzeros(A) for j in group[c] @@ -386,7 +430,7 @@ end function decompress!(A::AbstractMatrix, B::AbstractMatrix, result::RowColoringResult) (; color) = result S = result.bg.S2 - check_same_pattern(A, S) + @assert size(A) == size(S) fill!(A, zero(eltype(A))) rvS = rowvals(S) for j in axes(S, 2) @@ -404,7 +448,7 @@ function decompress_single_color!( ) (; group) = result S, Sᵀ = result.bg.S2, result.bg.S1 - check_same_pattern(A, S) + @assert size(A) == size(S) rvSᵀ = rowvals(Sᵀ) for i in group[c] for k in nzrange(Sᵀ, i) @@ -416,12 +460,28 @@ function decompress_single_color!( end function decompress!(A::SparseMatrixCSC, B::AbstractMatrix, result::RowColoringResult) - (; compressed_indices) = result + (; compressed_indices, allow_denser) = result S = result.bg.S2 - check_same_pattern(A, S) + check_compatible_pattern(A, S; allow_denser) nzA = nonzeros(A) - for k in eachindex(nzA, compressed_indices) - nzA[k] = B[compressed_indices[k]] + if nnz(A) == nnz(S) + for k in eachindex(nzA, compressed_indices) + nzA[k] = B[compressed_indices[k]] + end + else # nnz(A) > nnz(S) + rvA, rvS = rowvals(A), rowvals(S) + shift = 0 + for j in axes(S, 2) + for k in nzrange(S, j) + i = rvS[k] + while (k + shift) < A.colptr[j] || rvA[k + shift] < i + nzA[k + shift] = zero(eltype(A)) + shift += 1 + end + nzA[k + shift] = B[compressed_indices[k]] + end + end + nzA[(nnz(S) + shift + 1):end] .= zero(eltype(A)) end return A end @@ -433,7 +493,7 @@ function decompress!( ) (; ag, compressed_indices) = result (; S) = ag - uplo == :F && check_same_pattern(A, S) + @assert size(A) == size(S) fill!(A, zero(eltype(A))) rvS = rowvals(S) @@ -457,7 +517,7 @@ function decompress_single_color!( ) (; ag, compressed_indices, group) = result (; S) = ag - uplo == :F && check_same_pattern(A, S) + @assert size(A) == size(S) lower_index = (c - 1) * S.n + 1 upper_index = c * S.n @@ -488,25 +548,54 @@ end function decompress!( A::SparseMatrixCSC, B::AbstractMatrix, result::StarSetColoringResult, uplo::Symbol=:F ) - (; ag, compressed_indices) = result + (; ag, compressed_indices, allow_denser) = result (; S) = ag nzA = nonzeros(A) if uplo == :F - check_same_pattern(A, S) - for k in eachindex(nzA, compressed_indices) - nzA[k] = B[compressed_indices[k]] + check_compatible_pattern(A, S; allow_denser) + if nnz(A) == nnz(S) + for k in eachindex(nzA, compressed_indices) + nzA[k] = B[compressed_indices[k]] + end + else # nnz(A) > nnz(S) + rvA, rvS = rowvals(A), rowvals(S) + shift = 0 + for j in axes(S, 2) + for k in nzrange(S, j) + i = rvS[k] + while (k + shift) < A.colptr[j] || rvA[k + shift] < i + nzA[k + shift] = zero(eltype(A)) + shift += 1 + end + nzA[k + shift] = B[compressed_indices[k]] + end + end + nzA[(nnz(S) + shift + 1):end] .= zero(eltype(A)) end else - rvS = rowvals(S) + rvA, rvS = rowvals(A), rowvals(S) l = 0 # assume A has the same pattern as the triangle + shift = 0 for j in axes(S, 2) for k in nzrange(S, j) i = rvS[k] if in_triangle(i, j, uplo) l += 1 - nzA[l] = B[compressed_indices[k]] + while (l + shift) < A.colptr[j] || rvA[l + shift] < i + nzA[l + shift] = zero(eltype(A)) + shift += 1 + end + nzA[l + shift] = B[compressed_indices[k]] end end + nzA[(l + shift + 1):end] .= zero(eltype(A)) + if !allow_denser && shift > 0 + throw( + DimensionMismatch( + "Decompression target must have exactly as many nonzeros as the sparsity pattern used for coloring", + ), + ) + end end end return A @@ -519,7 +608,7 @@ function decompress!( ) (; ag, color, reverse_bfs_orders, buffer) = result (; S) = ag - uplo == :F && check_same_pattern(A, S) + @assert size(A) == size(S) R = eltype(A) fill!(A, zero(R)) @@ -582,7 +671,7 @@ function decompress!( (; S) = ag A_colptr = A.colptr nzA = nonzeros(A) - uplo == :F && check_same_pattern(A, S) + uplo == :F && check_compatible_pattern(A, S) if eltype(buffer) == R buffer_right_type = buffer @@ -679,7 +768,7 @@ function decompress!( ) (; color, strict_upper_nonzero_inds, M_factorization, strict_upper_nonzeros_A) = result S = result.ag.S - uplo == :F && check_same_pattern(A, S) + @assert size(A) == size(S) # TODO: for some reason I cannot use ldiv! with a sparse QR strict_upper_nonzeros_A = M_factorization \ vec(B) @@ -740,6 +829,7 @@ function decompress!( A::AbstractMatrix, Br::AbstractMatrix, Bc::AbstractMatrix, result::BicoloringResult ) m, n = size(A) + @assert size(A) == size(result.S) Br_and_Bc = _join_compressed!(result, Br, Bc) A_and_Aᵀ = decompress(Br_and_Bc, result.symmetric_result) copyto!(A, A_and_Aᵀ[(n + 1):(n + m), 1:n]) # original matrix in bottom left corner @@ -749,7 +839,8 @@ end function decompress!( A::SparseMatrixCSC, Br::AbstractMatrix, Bc::AbstractMatrix, result::BicoloringResult ) - (; large_colptr, large_rowval, symmetric_result) = result + (; S, large_colptr, large_rowval, symmetric_result) = result + check_compatible_pattern(A, S) m, n = size(A) Br_and_Bc = _join_compressed!(result, Br, Bc) # pretend A is larger diff --git a/src/graph.jl b/src/graph.jl index 3a6ed5cd..b14f9b63 100644 --- a/src/graph.jl +++ b/src/graph.jl @@ -99,8 +99,10 @@ end Return a [`SparsityPatternCSC`](@ref) corresponding to the matrix `[0 Aᵀ; A 0]`, with a minimum of allocations. """ -bidirectional_pattern(A::AbstractMatrix; symmetric_pattern::Bool) = - bidirectional_pattern(SparsityPatternCSC(SparseMatrixCSC(A)); symmetric_pattern) +function bidirectional_pattern(A::AbstractMatrix; symmetric_pattern::Bool) + S = SparsityPatternCSC(SparseMatrixCSC(A)) + return bidirectional_pattern(S; symmetric_pattern) +end function bidirectional_pattern(S::SparsityPatternCSC{T}; symmetric_pattern::Bool) where {T} m, n = size(S) @@ -171,7 +173,7 @@ function bidirectional_pattern(S::SparsityPatternCSC{T}; symmetric_pattern::Bool # Create the SparsityPatternCSC of the augmented adjacency matrix S_and_Sᵀ = SparsityPatternCSC{T}(p, p, colptr, rowval) - return S_and_Sᵀ, edge_to_index + return S, S_and_Sᵀ, edge_to_index end function build_edge_to_index(S::SparsityPatternCSC{T}) where {T} diff --git a/src/interface.jl b/src/interface.jl index f54b5f73..6e244ee9 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -69,12 +69,23 @@ It is passed as an argument to the main function [`coloring`](@ref). # Constructors - GreedyColoringAlgorithm{decompression}(order=NaturalOrder(); postprocessing=false) - GreedyColoringAlgorithm(order=NaturalOrder(); postprocessing=false, decompression=:direct) + GreedyColoringAlgorithm{decompression}( + order=NaturalOrder(); + postprocessing=false, + allow_denser=false, + ) + + GreedyColoringAlgorithm( + order=NaturalOrder(); + decompression=:direct + postprocessing=false, + allow_denser=false, + ) - `order::AbstractOrder`: the order in which the columns or rows are colored, which can impact the number of colors. -- `postprocessing::Bool`: whether or not the coloring will be refined by assigning the neutral color `0` to some vertices. - `decompression::Symbol`: either `:direct` or `:substitution`. Usually `:substitution` leads to fewer colors, at the cost of a more expensive coloring (and decompression). When `:substitution` is not applicable, it falls back on `:direct` decompression. +- `postprocessing::Bool`: whether or not the coloring will be refined by assigning the neutral color `0` to some vertices. +- `allow_denser::Bool`: whether or not to allow decompression into a `SparseMatrixCSC` which has more nonzeros than the original sparsity pattern (see [`decompress!`](@ref) to know when this is implemented). !!! warning The second constructor (based on keyword arguments) is type-unstable. @@ -92,28 +103,36 @@ See their respective docstrings for details. # See also - [`AbstractOrder`](@ref) -- [`decompress`](@ref) +- [`decompress`](@ref), [`decompress!`](@ref) """ struct GreedyColoringAlgorithm{decompression,O<:AbstractOrder} <: ADTypes.AbstractColoringAlgorithm order::O postprocessing::Bool + allow_denser::Bool end function GreedyColoringAlgorithm{decompression}( - order::AbstractOrder=NaturalOrder(); postprocessing::Bool=false + order::AbstractOrder=NaturalOrder(); + postprocessing::Bool=false, + allow_denser::Bool=false, ) where {decompression} check_valid_algorithm(decompression) - return GreedyColoringAlgorithm{decompression,typeof(order)}(order, postprocessing) + return GreedyColoringAlgorithm{decompression,typeof(order)}( + order, postprocessing, allow_denser + ) end function GreedyColoringAlgorithm( order::AbstractOrder=NaturalOrder(); - postprocessing::Bool=false, decompression::Symbol=:direct, + postprocessing::Bool=false, + allow_denser::Bool=false, ) check_valid_algorithm(decompression) - return GreedyColoringAlgorithm{decompression,typeof(order)}(order, postprocessing) + return GreedyColoringAlgorithm{decompression,typeof(order)}( + order, postprocessing, allow_denser + ) end ## Coloring @@ -232,7 +251,7 @@ function _coloring( ) color = partial_distance2_coloring(bg, Val(2), algo.order) if speed_setting isa WithResult - return ColumnColoringResult(A, bg, color) + return ColumnColoringResult(A, bg, color; allow_denser=algo.allow_denser) else return color end @@ -251,7 +270,7 @@ function _coloring( ) color = partial_distance2_coloring(bg, Val(1), algo.order) if speed_setting isa WithResult - return RowColoringResult(A, bg, color) + return RowColoringResult(A, bg, color; allow_denser=algo.allow_denser) else return color end @@ -268,7 +287,7 @@ function _coloring( ag = AdjacencyGraph(A; has_diagonal=true) color, star_set = star_coloring(ag, algo.order, algo.postprocessing) if speed_setting isa WithResult - return StarSetColoringResult(A, ag, color, star_set) + return StarSetColoringResult(A, ag, color, star_set; allow_denser=algo.allow_denser) else return color end @@ -299,12 +318,14 @@ function _coloring( decompression_eltype::Type{R}, symmetric_pattern::Bool, ) where {R} - A_and_Aᵀ, edge_to_index = bidirectional_pattern(A; symmetric_pattern) - ag = AdjacencyGraph(A_and_Aᵀ, edge_to_index; has_diagonal=false) + S, S_and_Sᵀ, edge_to_index = bidirectional_pattern(A; symmetric_pattern) + ag = AdjacencyGraph(S_and_Sᵀ, edge_to_index; has_diagonal=false) color, star_set = star_coloring(ag, algo.order, algo.postprocessing) if speed_setting isa WithResult - symmetric_result = StarSetColoringResult(A_and_Aᵀ, ag, color, star_set) - return BicoloringResult(A, ag, symmetric_result, R) + symmetric_result = StarSetColoringResult( + S_and_Sᵀ, ag, color, star_set; allow_denser=false + ) + return BicoloringResult(A, S, ag, symmetric_result, R) else row_color, column_color, _ = remap_colors( eltype(ag), color, maximum(color), size(A)... @@ -321,12 +342,12 @@ function _coloring( decompression_eltype::Type{R}, symmetric_pattern::Bool, ) where {R} - A_and_Aᵀ, edge_to_index = bidirectional_pattern(A; symmetric_pattern) - ag = AdjacencyGraph(A_and_Aᵀ, edge_to_index; has_diagonal=false) + S, S_and_Sᵀ, edge_to_index = bidirectional_pattern(A; symmetric_pattern) + ag = AdjacencyGraph(S_and_Sᵀ, edge_to_index; has_diagonal=false) color, tree_set = acyclic_coloring(ag, algo.order, algo.postprocessing) if speed_setting isa WithResult - symmetric_result = TreeSetColoringResult(A_and_Aᵀ, ag, color, tree_set, R) - return BicoloringResult(A, ag, symmetric_result, R) + symmetric_result = TreeSetColoringResult(S_and_Sᵀ, ag, color, tree_set, R) + return BicoloringResult(A, S, ag, symmetric_result, R) else row_color, column_color, _ = remap_colors( eltype(ag), color, maximum(color), size(A)... diff --git a/src/matrices.jl b/src/matrices.jl index 795983c8..268eb7b8 100644 --- a/src/matrices.jl +++ b/src/matrices.jl @@ -61,25 +61,21 @@ function respectful_similar(A::Union{Symmetric,Hermitian}, ::Type{T}) where {T} return respectful_similar(sparse(A), T) end -""" - same_pattern(A, B) - -Perform a partial equality check on the sparsity patterns of `A` and `B`: - -- if the return is `true`, they might have the same sparsity pattern but we're not sure -- if the return is `false`, they definitely don't have the same sparsity pattern -""" -same_pattern(A, B) = size(A) == size(B) - -function same_pattern( +function compatible_pattern( A::Union{SparseMatrixCSC,SparsityPatternCSC}, - B::Union{SparseMatrixCSC,SparsityPatternCSC}, + S::Union{SparseMatrixCSC,SparsityPatternCSC}; + allow_denser::Bool=false, ) - return size(A) == size(B) && nnz(A) == nnz(B) + return allow_denser ? nnz(A) >= nnz(S) : nnz(A) == nnz(S) end -function check_same_pattern(A, S) - if !same_pattern(A, S) - throw(DimensionMismatch("`A` and `S` must have the same sparsity pattern.")) +function check_compatible_pattern(A, S; allow_denser::Bool=false) + if size(A) != size(S) + msg = "Decompression target must have the same size as the sparsity pattern used for coloring" + throw(DimensionMismatch(msg)) + elseif !compatible_pattern(A, S; allow_denser) + msg = """Decompression target must have $(allow_denser ? "at least" : "exactly") as many nonzeros as the sparsity pattern used for coloring""" + throw(DimensionMismatch(msg)) end + return true end diff --git a/src/result.jl b/src/result.jl index b1ec9409..06473a59 100644 --- a/src/result.jl +++ b/src/result.jl @@ -158,10 +158,12 @@ struct ColumnColoringResult{ group::GT "flattened indices mapping the compressed matrix `B` to the uncompressed matrix `A` when `A isa SparseMatrixCSC`. They satisfy `nonzeros(A)[k] = vec(B)[compressed_indices[k]]`" compressed_indices::Vector{T} + "whether to allow full decompression into a `SparseMatrixCSC` with a strictly larger sparsity pattern" + allow_denser::Bool end function ColumnColoringResult( - A::AbstractMatrix, bg::BipartiteGraph{T}, color::Vector{<:Integer} + A::AbstractMatrix, bg::BipartiteGraph{T}, color::Vector{<:Integer}; allow_denser::Bool ) where {T<:Integer} S = bg.S2 group = group_by_color(T, color) @@ -176,7 +178,7 @@ function ColumnColoringResult( compressed_indices[k] = (c - 1) * n + i end end - return ColumnColoringResult(A, bg, color, group, compressed_indices) + return ColumnColoringResult(A, bg, color, group, compressed_indices, allow_denser) end """ @@ -202,10 +204,11 @@ struct RowColoringResult{ color::Vector{T} group::GT compressed_indices::Vector{T} + allow_denser::Bool end function RowColoringResult( - A::AbstractMatrix, bg::BipartiteGraph{T}, color::Vector{<:Integer} + A::AbstractMatrix, bg::BipartiteGraph{T}, color::Vector{<:Integer}; allow_denser::Bool ) where {T<:Integer} S = bg.S2 group = group_by_color(T, color) @@ -220,7 +223,7 @@ function RowColoringResult( compressed_indices[k] = (j - 1) * C + c end end - return RowColoringResult(A, bg, color, group, compressed_indices) + return RowColoringResult(A, bg, color, group, compressed_indices, allow_denser) end """ @@ -247,13 +250,15 @@ struct StarSetColoringResult{ group::GT star_set::StarSet{T} compressed_indices::Vector{T} + allow_denser::Bool end function StarSetColoringResult( A::AbstractMatrix, ag::AdjacencyGraph{T}, color::Vector{<:Integer}, - star_set::StarSet{<:Integer}, + star_set::StarSet{<:Integer}; + allow_denser::Bool, ) where {T<:Integer} (; star, hub) = star_set S = pattern(ag) @@ -289,7 +294,9 @@ function StarSetColoringResult( end end - return StarSetColoringResult(A, ag, color, group, star_set, compressed_indices) + return StarSetColoringResult( + A, ag, color, group, star_set, compressed_indices, allow_denser + ) end """ @@ -574,6 +581,8 @@ struct BicoloringResult{ } <: AbstractColoringResult{:nonsymmetric,:bidirectional,decompression} "matrix that was colored" A::M + "sparsity pattern of `A`" + S::SparsityPatternCSC{T} "augmented adjacency graph that was used for bicoloring" abg::G "one integer color for each column" @@ -606,6 +615,7 @@ row_groups(result::BicoloringResult) = result.row_group function BicoloringResult( A::AbstractMatrix, + S::SparsityPatternCSC{T}, ag::AdjacencyGraph{T}, symmetric_result::AbstractColoringResult{:symmetric,:column}, decompression_eltype::Type{R}, @@ -624,6 +634,7 @@ function BicoloringResult( large_rowval = ag.S.rowval[1:(end ÷ 2)] # forget the second half of nonzeros return BicoloringResult( A, + S, ag, column_color, row_color, diff --git a/test/constant.jl b/test/constant.jl index 5de881b8..e3a4dce8 100644 --- a/test/constant.jl +++ b/test/constant.jl @@ -1,37 +1,62 @@ using ADTypes: ADTypes using SparseMatrixColorings +using StableRNGs using Test -matrix_template = ones(100, 200) +rng = StableRNG(63) + +A = sprand(rng, 100, 200, 0.05) @testset "Column coloring" begin problem = ColoringProblem(; structure=:nonsymmetric, partition=:column) - color = rand(1:5, size(matrix_template, 2)) - algo = ConstantColoringAlgorithm(matrix_template, color; partition=:column) - wrong_algo = ConstantColoringAlgorithm(matrix_template, color; partition=:row) - @test_throws DimensionMismatch coloring(transpose(matrix_template), problem, algo) - @test_throws MethodError coloring(matrix_template, problem, wrong_algo) - result = coloring(matrix_template, problem, algo) + color = fast_coloring(A, problem, GreedyColoringAlgorithm()) + algo = ConstantColoringAlgorithm(A, color; partition=:column) + algo_tolerant = ConstantColoringAlgorithm( + A, color; partition=:column, allow_denser=true + ) + wrong_algo = ConstantColoringAlgorithm(A, color; partition=:row) + @test_throws DimensionMismatch coloring(transpose(A), problem, algo) + @test_throws MethodError coloring(A, problem, wrong_algo) + result = coloring(A, problem, algo) + result_tolerant = coloring(A, problem, algo_tolerant) + B = compress(A, result) @test column_colors(result) == color - @test ADTypes.column_coloring(matrix_template, algo) == color - @test_throws MethodError ADTypes.row_coloring(matrix_template, algo) + @test ADTypes.column_coloring(A, algo) == color + @test_throws MethodError ADTypes.row_coloring(A, algo) + A2 = copy(A) + for added_coeff in 1:10 + i, j = rand(rng, axes(A, 1)), rand(rng, axes(A, 2)) + A2[i, j] = 1 + end + @test_throws DimensionMismatch decompress!(A2, B, result) + @test decompress!(A2, B, result_tolerant) == A end @testset "Row coloring" begin problem = ColoringProblem(; structure=:nonsymmetric, partition=:row) - color = rand(1:5, size(matrix_template, 1)) - algo = ConstantColoringAlgorithm(matrix_template, color; partition=:row) - @test_throws DimensionMismatch coloring(transpose(matrix_template), problem, algo) - result = coloring(matrix_template, problem, algo) + color = fast_coloring(A, problem, GreedyColoringAlgorithm()) + algo = ConstantColoringAlgorithm(A, color; partition=:row) + algo_tolerant = ConstantColoringAlgorithm(A, color; partition=:row, allow_denser=true) + @test_throws DimensionMismatch coloring(transpose(A), problem, algo) + result = coloring(A, problem, algo) + result_tolerant = coloring(A, problem, algo_tolerant) + B = compress(A, result) @test row_colors(result) == color - @test ADTypes.row_coloring(matrix_template, algo) == color - @test_throws MethodError ADTypes.column_coloring(matrix_template, algo) + @test ADTypes.row_coloring(A, algo) == color + @test_throws MethodError ADTypes.column_coloring(A, algo) + A2 = copy(A) + for added_coeff in 1:10 + i, j = rand(rng, axes(A, 1)), rand(rng, axes(A, 2)) + A2[i, j] = 1 + end + @test decompress!(A2, B, result_tolerant) == A + @test_throws DimensionMismatch decompress!(A2, B, result) end @testset "Symmetric coloring" begin wrong_problem = ColoringProblem(; structure=:symmetric, partition=:column) - color = rand(1:5, size(matrix_template, 2)) - algo = ConstantColoringAlgorithm(matrix_template, color; partition=:column) - @test_throws MethodError coloring(matrix_template, wrong_problem, algo) - @test_throws MethodError ADTypes.symmetric_coloring(matrix_template, algo) + color = ones(Int, size(A, 2)) + algo = ConstantColoringAlgorithm(A, color; partition=:column) + @test_throws MethodError coloring(A, wrong_problem, algo) + @test_throws MethodError ADTypes.symmetric_coloring(A, algo) end diff --git a/test/graph.jl b/test/graph.jl index 9c8f66c1..155ac079 100644 --- a/test/graph.jl +++ b/test/graph.jl @@ -35,7 +35,9 @@ using Test p = 0.05 * rand() A = sprand(Bool, m, n, p) A_and_Aᵀ = [spzeros(Bool, n, n) transpose(A); A spzeros(Bool, m, m)] - S_and_Sᵀ, edge_to_index = bidirectional_pattern(A; symmetric_pattern=false) + _, S_and_Sᵀ, edge_to_index = bidirectional_pattern( + A; symmetric_pattern=false + ) @test S_and_Sᵀ.colptr == A_and_Aᵀ.colptr @test S_and_Sᵀ.rowval == A_and_Aᵀ.rowval M = SparseMatrixCSC( @@ -50,7 +52,9 @@ using Test p = 0.05 * rand() A = sparse(Symmetric(sprand(Bool, m, m, p))) A_and_Aᵀ = [spzeros(Bool, m, m) transpose(A); A spzeros(Bool, m, m)] - S_and_Sᵀ, edge_to_index = bidirectional_pattern(A; symmetric_pattern=true) + _, S_and_Sᵀ, edge_to_index = bidirectional_pattern( + A; symmetric_pattern=true + ) @test S_and_Sᵀ.colptr == A_and_Aᵀ.colptr @test S_and_Sᵀ.rowval == A_and_Aᵀ.rowval M = SparseMatrixCSC( diff --git a/test/matrices.jl b/test/matrices.jl index 1571497b..b20e0c37 100644 --- a/test/matrices.jl +++ b/test/matrices.jl @@ -1,7 +1,7 @@ using LinearAlgebra using SparseArrays using SparseMatrixColorings: - check_same_pattern, matrix_versions, respectful_similar, same_pattern + check_compatible_pattern, matrix_versions, respectful_similar, compatible_pattern using StableRNGs using Test @@ -44,9 +44,11 @@ end A2 = copy(S) A2[1, 1] = 1 - @test same_pattern(A1, S) - @test !same_pattern(A2, S) - @test same_pattern(Matrix(A2), S) + @test compatible_pattern(A1, S) + @test !compatible_pattern(A2, S) - @test_throws DimensionMismatch check_same_pattern(A2, S) + @test_throws DimensionMismatch check_compatible_pattern(vcat(A1, A1), S) + @test_throws DimensionMismatch check_compatible_pattern(A2, S) + @test check_compatible_pattern(A1, S; allow_denser=true) + @test check_compatible_pattern(A2, S; allow_denser=true) end diff --git a/test/random.jl b/test/random.jl index 02c1a3a1..06485c54 100644 --- a/test/random.jl +++ b/test/random.jl @@ -21,7 +21,7 @@ symmetric_params = vcat( @testset "Column coloring & decompression" begin problem = ColoringProblem(; structure=:nonsymmetric, partition=:column) - algo = GreedyColoringAlgorithm(; decompression=:direct) + algo = GreedyColoringAlgorithm(; decompression=:direct, allow_denser=true) @testset "$((; m, n, p))" for (m, n, p) in asymmetric_params A0 = sprand(rng, m, n, p) color0 = column_coloring(A0, algo) @@ -36,7 +36,7 @@ end; @testset "Row coloring & decompression" begin problem = ColoringProblem(; structure=:nonsymmetric, partition=:row) - algo = GreedyColoringAlgorithm(; decompression=:direct) + algo = GreedyColoringAlgorithm(; decompression=:direct, allow_denser=true) @testset "$((; m, n, p))" for (m, n, p) in asymmetric_params A0 = sprand(rng, m, n, p) color0 = row_coloring(A0, algo) @@ -52,8 +52,12 @@ end; @testset "Symmetric coloring & direct decompression" begin problem = ColoringProblem(; structure=:symmetric, partition=:column) @testset for algo in ( - GreedyColoringAlgorithm(; postprocessing=false, decompression=:direct), - GreedyColoringAlgorithm(; postprocessing=true, decompression=:direct), + GreedyColoringAlgorithm(; + postprocessing=false, decompression=:direct, allow_denser=true + ), + GreedyColoringAlgorithm(; + postprocessing=true, decompression=:direct, allow_denser=true + ), ) @testset "$((; n, p))" for (n, p) in symmetric_params A0 = sparse(Symmetric(sprand(rng, n, n, p))) diff --git a/test/small.jl b/test/small.jl index 2e12372c..c92b8468 100644 --- a/test/small.jl +++ b/test/small.jl @@ -1,3 +1,4 @@ +using LinearAlgebra using SparseArrays using SparseMatrixColorings using SparseMatrixColorings: @@ -196,3 +197,49 @@ end; ) end end; + +@testset verbose = true "Mismatched sparsity pattern during decompression" begin + A = sparse(Diagonal(ones(10))) + A_denser = copy(A) + A_denser[1, 2] = A_denser[2, 1] = 1 + A_sparser = copy(A) + A_sparser[1, 1] = 0 + dropzeros!(A_sparser) + + @testset "$structure - $partition - $decompression" for ( + structure, partition, decompression + ) in [ + (:nonsymmetric, :column, :direct), + (:nonsymmetric, :row, :direct), + (:symmetric, :column, :direct), + (:symmetric, :column, :substitution), + (:nonsymmetric, :bidirectional, :direct), + (:nonsymmetric, :bidirectional, :substitution), + ] + problem = ColoringProblem(; structure, partition) + algo = GreedyColoringAlgorithm(; decompression) + algo_tolerant = GreedyColoringAlgorithm(; decompression, allow_denser=true) + result = coloring(A, problem, algo) + result_tolerant = coloring(A, problem, algo_tolerant) + Bs = if partition == :bidirectional + compress(A, result) + else + (compress(A, result),) + end + @test_throws DimensionMismatch decompress!(copy(A_denser), Bs..., result) + @test_throws DimensionMismatch decompress!(copy(A_sparser), Bs..., result_tolerant) + if decompression == :direct && partition in (:row, :column) + @test decompress!(copy(A_denser), Bs..., result_tolerant) == A + else + @test_throws DimensionMismatch decompress!( + copy(A_denser), Bs..., result_tolerant + ) + end + if structure == :symmetric && decompression == :direct + @test decompress!(copy(tril(A_denser)), Bs..., result_tolerant, :L) == tril(A) + @test_throws DimensionMismatch decompress!( + copy(tril(A_denser)), Bs..., result, :L + ) + end + end +end; diff --git a/test/utils.jl b/test/utils.jl index fb3d42c5..4f6fb777 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -86,6 +86,18 @@ function test_coloring_decompression( @test decompress(B, result) ≈ A0 # check result wasn't modified @test decompress!(respectful_similar(A, eltype(B)), B, result) ≈ A0 @test decompress!(respectful_similar(A, eltype(B)), B, result) ≈ A0 + if algo.allow_denser && decompression == :direct && A isa SparseMatrixCSC + A_bigger = respectful_similar(A, eltype(B)) + for _ in 1:10 + nb_coeffs_added = rand(1:minimum(size(A))) + for _ in nb_coeffs_added + i = rand(axes(A, 1)) + j = rand(axes(A, 2)) + A_bigger[i, j] = one(eltype(B)) + end + @test decompress!(A_bigger, B, result) ≈ A0 + end + end end @testset "Single-color decompression" begin @@ -120,6 +132,22 @@ function test_coloring_decompression( @test A3upper ≈ triu(A0) @test A3lower ≈ tril(A0) @test A3both ≈ A0 + + if algo.allow_denser && decompression == :direct && A isa SparseMatrixCSC + A3upper_bigger = respectful_similar(A3upper, eltype(B)) + A3lower_bigger = respectful_similar(A3lower, eltype(B)) + for _ in 1:10 + nb_coeffs_added = rand(1:minimum(size(A))) + for _ in nb_coeffs_added + i = rand(axes(A, 1)) + j = rand(axes(A, 2)) + A3upper_bigger[min(i, j), max(i, j)] = one(eltype(B)) + A3lower_bigger[max(i, j), min(i, j)] = one(eltype(B)) + end + @test decompress!(A3upper_bigger, B, result, :U) ≈ triu(A0) + @test decompress!(A3lower_bigger, B, result, :L) ≈ tril(A0) + end + end end end @@ -183,6 +211,7 @@ function test_bicoloring_decompression( end Br, Bc = compress(A, result) row_color, column_color = row_colors(result), column_colors(result) + T = promote_eltype(Br, Bc) @testset "Coherence" begin @test size(Br, 1) == length(unique(row_color[row_color .> 0])) @@ -202,12 +231,8 @@ function test_bicoloring_decompression( @testset "Full decompression" begin @test decompress(Br, Bc, result) ≈ A0 @test decompress(Br, Bc, result) ≈ A0 # check result wasn't modified - @test decompress!( - respectful_similar(A, promote_eltype(Br, Bc)), Br, Bc, result - ) ≈ A0 - @test decompress!( - respectful_similar(A, promote_eltype(Br, Bc)), Br, Bc, result - ) ≈ A0 + @test decompress!(respectful_similar(A, T), Br, Bc, result) ≈ A0 + @test decompress!(respectful_similar(A, T), Br, Bc, result) ≈ A0 end end end