From 6b0fbb19594216c994d923cce5bcabc73a3b006c Mon Sep 17 00:00:00 2001 From: Alexis Montoison Date: Tue, 4 Nov 2025 22:30:02 -0600 Subject: [PATCH] Revisit the post-processing --- src/coloring.jl | 15 ++- src/graph.jl | 8 +- src/interface.jl | 28 +++-- src/postprocessing.jl | 250 +++++++++++++++++++++++++++++++++++++----- test/random.jl | 16 ++- test/small.jl | 220 +++++++++++++++++++++---------------- test/utils.jl | 8 +- 7 files changed, 394 insertions(+), 151 deletions(-) diff --git a/src/coloring.jl b/src/coloring.jl index cf3d8f79..dc9ca78c 100644 --- a/src/coloring.jl +++ b/src/coloring.jl @@ -81,7 +81,7 @@ end """ star_coloring( g::AdjacencyGraph, vertices_in_order::AbstractVector, postprocessing::Bool; - forced_colors::Union{AbstractVector,Nothing}=nothing + postprocessing_minimizes::Symbol=:all_colors, forced_colors::Union{AbstractVector,Nothing}=nothing ) Compute a star coloring of all vertices in the adjacency graph `g` and return a tuple `(color, star_set)`, where @@ -110,6 +110,7 @@ function star_coloring( g::AdjacencyGraph{T}, vertices_in_order::AbstractVector{<:Integer}, postprocessing::Bool; + postprocessing_minimizes::Symbol=:all_colors, forced_colors::Union{AbstractVector{<:Integer},Nothing}=nothing, ) where {T<:Integer} # Initialize data structures @@ -168,7 +169,7 @@ function star_coloring( if postprocessing # Reuse the vector forbidden_colors to compute offsets during post-processing offsets = forbidden_colors - postprocess!(color, star_set, g, offsets) + postprocess!(color, star_set, g, offsets, postprocessing_minimizes) end return color, star_set end @@ -250,7 +251,8 @@ struct StarSet{T} end """ - acyclic_coloring(g::AdjacencyGraph, vertices_in_order::AbstractVector, postprocessing::Bool) + acyclic_coloring(g::AdjacencyGraph, vertices_in_order::AbstractVector, postprocessing::Bool; + postprocessing_minimizes::Symbol=:all_colors) Compute an acyclic coloring of all vertices in the adjacency graph `g` and return a tuple `(color, tree_set)`, where @@ -273,7 +275,10 @@ If `postprocessing=true`, some colors might be replaced with `0` (the "neutral" > [_New Acyclic and Star Coloring Algorithms with Application to Computing Hessians_](https://epubs.siam.org/doi/abs/10.1137/050639879), Gebremedhin et al. (2007), Algorithm 3.1 """ function acyclic_coloring( - g::AdjacencyGraph{T}, vertices_in_order::AbstractVector{<:Integer}, postprocessing::Bool + g::AdjacencyGraph{T}, + vertices_in_order::AbstractVector{<:Integer}, + postprocessing::Bool; + postprocessing_minimizes::Symbol=:all_colors, ) where {T<:Integer} # Initialize data structures nv = nb_vertices(g) @@ -345,7 +350,7 @@ function acyclic_coloring( if postprocessing # Reuse the vector forbidden_colors to compute offsets during post-processing offsets = forbidden_colors - postprocess!(color, tree_set, g, offsets) + postprocess!(color, tree_set, g, offsets, postprocessing_minimizes) end return color, tree_set end diff --git a/src/graph.jl b/src/graph.jl index 84c3353f..a79935d8 100644 --- a/src/graph.jl +++ b/src/graph.jl @@ -227,6 +227,7 @@ The adjacency graph of a symmetric matrix `A ∈ ℝ^{n × n}` is `G(A) = (V, E) struct AdjacencyGraph{T<:Integer,augmented_graph} S::SparsityPatternCSC{T} edge_to_index::Vector{T} + original_size::Tuple{Int,Int} end Base.eltype(::AdjacencyGraph{T}) where {T} = T @@ -235,15 +236,16 @@ function AdjacencyGraph( S::SparsityPatternCSC{T}, edge_to_index::Vector{T}=build_edge_to_index(S); augmented_graph::Bool=false, + original_size::Tuple{Int,Int}=size(S), ) where {T} - return AdjacencyGraph{T,augmented_graph}(S, edge_to_index) + return AdjacencyGraph{T,augmented_graph}(S, edge_to_index, original_size) end -function AdjacencyGraph(A::SparseMatrixCSC; augmented_graph::Bool=false) +function AdjacencyGraph(A::SparseMatrixCSC; augmented_graph::Bool=false, original_size::Tuple{Int,Int}=size(A)) return AdjacencyGraph(SparsityPatternCSC(A); augmented_graph) end -function AdjacencyGraph(A::AbstractMatrix; augmented_graph::Bool=false) +function AdjacencyGraph(A::AbstractMatrix; augmented_graph::Bool=false, original_size::Tuple{Int,Int}=size(A)) return AdjacencyGraph(SparseMatrixCSC(A); augmented_graph) end diff --git a/src/interface.jl b/src/interface.jl index cc57bcc7..63830bba 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -69,11 +69,12 @@ 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, postprocessing_minimizes=:all_colors) + GreedyColoringAlgorithm(order=NaturalOrder(); postprocessing=false, postprocessing_minimizes=:all_colors, decompression=:direct) - `order::Union{AbstractOrder,Tuple}`: the order in which the columns or rows are colored, which can impact the number of colors. Can also be a tuple of different orders to try out, from which the best order (the one with the lowest total number of colors) will be used. -- `postprocessing::Bool`: whether or not the coloring will be refined by assigning the neutral color `0` to some vertices. +- `postprocessing::Bool`: whether or not the coloring will be refined by assigning the neutral color `0` to some vertices. This option does not affect row or column colorings. +- `postprocessing_minimizes::Symbol`: which number of distinct colors is heuristically minimized by postprocessing, either `:all_colors`, `:row_colors` or `:column_colors`. This option only affects bidirectional colorings. - `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. !!! warning @@ -98,10 +99,12 @@ struct GreedyColoringAlgorithm{decompression,N,O<:NTuple{N,AbstractOrder}} <: ADTypes.AbstractColoringAlgorithm orders::O postprocessing::Bool + postprocessing_minimizes::Symbol function GreedyColoringAlgorithm{decompression}( order_or_orders::Union{AbstractOrder,Tuple}=NaturalOrder(); postprocessing::Bool=false, + postprocessing_minimizes::Symbol=:all_colors, ) where {decompression} check_valid_algorithm(decompression) if order_or_orders isa AbstractOrder @@ -109,7 +112,7 @@ struct GreedyColoringAlgorithm{decompression,N,O<:NTuple{N,AbstractOrder}} <: else orders = order_or_orders end - return new{decompression,length(orders),typeof(orders)}(orders, postprocessing) + return new{decompression,length(orders),typeof(orders)}(orders, postprocessing, postprocessing_minimizes) end end @@ -117,8 +120,9 @@ function GreedyColoringAlgorithm( order_or_orders::Union{AbstractOrder,Tuple}=NaturalOrder(); postprocessing::Bool=false, decompression::Symbol=:direct, + postprocessing_minimizes::Symbol=:all_colors, ) - return GreedyColoringAlgorithm{decompression}(order_or_orders; postprocessing) + return GreedyColoringAlgorithm{decompression}(order_or_orders; postprocessing, postprocessing_minimizes) end ## Coloring @@ -279,7 +283,7 @@ function _coloring( symmetric_pattern::Bool; forced_colors::Union{AbstractVector{<:Integer},Nothing}=nothing, ) - ag = AdjacencyGraph(A; augmented_graph=false) + ag = AdjacencyGraph(A; augmented_graph=false, original_size=size(A)) color_and_star_set_by_order = map(algo.orders) do order vertices_in_order = vertices(ag, order) return star_coloring(ag, vertices_in_order, algo.postprocessing; forced_colors) @@ -300,7 +304,7 @@ function _coloring( decompression_eltype::Type{R}, symmetric_pattern::Bool, ) where {R} - ag = AdjacencyGraph(A; augmented_graph=false) + ag = AdjacencyGraph(A; augmented_graph=false, original_size=size(A)) color_and_tree_set_by_order = map(algo.orders) do order vertices_in_order = vertices(ag, order) return acyclic_coloring(ag, vertices_in_order, algo.postprocessing) @@ -323,11 +327,12 @@ function _coloring( forced_colors::Union{AbstractVector{<:Integer},Nothing}=nothing, ) where {R} A_and_Aᵀ, edge_to_index = bidirectional_pattern(A; symmetric_pattern) - ag = AdjacencyGraph(A_and_Aᵀ, edge_to_index; augmented_graph=true) + ag = AdjacencyGraph(A_and_Aᵀ, edge_to_index; augmented_graph=true, original_size=size(A)) + postprocessing_minimizes = algo.postprocessing_minimizes outputs_by_order = map(algo.orders) do order vertices_in_order = vertices(ag, order) _color, _star_set = star_coloring( - ag, vertices_in_order, algo.postprocessing; forced_colors + ag, vertices_in_order, algo.postprocessing; postprocessing_minimizes, forced_colors ) (_row_color, _column_color, _symmetric_to_row, _symmetric_to_column) = remap_colors( eltype(ag), _color, maximum(_color), size(A)... @@ -370,10 +375,11 @@ function _coloring( symmetric_pattern::Bool, ) where {R} A_and_Aᵀ, edge_to_index = bidirectional_pattern(A; symmetric_pattern) - ag = AdjacencyGraph(A_and_Aᵀ, edge_to_index; augmented_graph=true) + ag = AdjacencyGraph(A_and_Aᵀ, edge_to_index; augmented_graph=true, original_size=size(A)) + postprocessing_minimizes = algo.postprocessing_minimizes outputs_by_order = map(algo.orders) do order vertices_in_order = vertices(ag, order) - _color, _tree_set = acyclic_coloring(ag, vertices_in_order, algo.postprocessing) + _color, _tree_set = acyclic_coloring(ag, vertices_in_order, algo.postprocessing; postprocessing_minimizes) (_row_color, _column_color, _symmetric_to_row, _symmetric_to_column) = remap_colors( eltype(ag), _color, maximum(_color), size(A)... ) diff --git a/src/postprocessing.jl b/src/postprocessing.jl index 9b0443b2..4981f6cc 100644 --- a/src/postprocessing.jl +++ b/src/postprocessing.jl @@ -5,15 +5,16 @@ function postprocess!( star_or_tree_set::Union{StarSet,TreeSet}, g::AdjacencyGraph, offsets::AbstractVector{<:Integer}, + postprocessing_minimizes::Symbol, ) - S = pattern(g) - edge_to_index = edge_indices(g) # flag which colors are actually used during decompression nb_colors = maximum(color) color_used = zeros(Bool, nb_colors) # nonzero diagonal coefficients force the use of their respective color (there can be no neutral colors if the diagonal is fully nonzero) - if !augmented_graph(g) + bicoloring = augmented_graph(g) + if !bicoloring + S = pattern(g) for i in axes(S, 1) if !iszero(S[i, i]) color_used[color[i]] = true @@ -21,12 +22,22 @@ function postprocess!( end end + # When bicoloring is false, column_color_used and row_color_used point to the same memory + row_color_used = bicoloring ? zeros(Bool, nb_colors) : color_used + column_color_used = color_used + if star_or_tree_set isa StarSet # star_or_tree_set is a StarSet - postprocess_with_star_set!(g, color_used, color, star_or_tree_set) + postprocess_with_star_set!(bicoloring, g, row_color_used, column_color_used, color, star_or_tree_set, postprocessing_minimizes) else # star_or_tree_set is a TreeSet - postprocess_with_tree_set!(color_used, color, star_or_tree_set) + postprocess_with_tree_set!(bicoloring, row_color_used, column_color_used, color, star_or_tree_set, postprocessing_minimizes) + end + + if bicoloring + # Identify colors that are used in either the row or column partition + # color_used = row_color_used .| column_color_used + color_used .|= row_color_used end # if at least one of the colors is useless, modify the color assignments of vertices @@ -58,10 +69,13 @@ function postprocess!( end function postprocess_with_star_set!( + bicoloring::Bool, g::AdjacencyGraph, - color_used::Vector{Bool}, + row_color_used::Vector{Bool}, + column_color_used::Vector{Bool}, color::AbstractVector{<:Integer}, star_set::StarSet, + postprocessing_minimizes::Symbol=:all_colors, ) S = pattern(g) edge_to_index = edge_indices(g) @@ -70,11 +84,18 @@ function postprocess_with_star_set!( (; star, hub) = star_set nb_trivial_stars = 0 + # size of the original matrix on which we want to perform bicoloring + (m, n) = g.original_size + # Iterate through all non-trivial stars for s in eachindex(hub) h = hub[s] if h > 0 - color_used[color[h]] = true + if h ≤ n + column_color_used[color[h]] = true + else + row_color_used[color[h]] = true + end else nb_trivial_stars += 1 end @@ -82,6 +103,14 @@ function postprocess_with_star_set!( # Process the trivial stars (if any) if nb_trivial_stars > 0 + # When bicoloring is false, row_color_counts and column_color_counts point to the same memory + nv = length(color) + nb_colors = length(row_color_used) + visited_vertices = zeros(Bool, nv) + row_color_counts = zeros(Int, nb_colors) + column_color_counts = bicoloring ? zeros(Int, nb_colors) : row_color_counts + all_trivial_stars_treated = true + rvS = rowvals(S) for j in axes(S, 2) for k in nzrange(S, j) @@ -91,27 +120,103 @@ function postprocess_with_star_set!( s = star[index_ij] h = hub[s] if h < 0 - h = abs(h) - spoke = h == j ? i : j - if color_used[color[spoke]] - # Switch the hub and the spoke to possibly avoid adding one more used color - hub[s] = spoke + if row_color_used[color[i]] + # The vertex i is already a hub in a non-trivial star + hub[s] = i else - # Keep the current hub - color_used[color[h]] = true + if column_color_used[color[j]] + # The vertex j is already a hub in a non-trivial star + hub[s] = j + else + all_trivial_stars_treated = false + # Count how many vertices of each color appear among the remaining trivial stars. + # Each vertex is counted at most once, using `visited_vertices` to avoid duplicates + # when a vertex belongs to multiple trivial stars. + if !visited_vertices[i] + visited_vertices[i] = true + row_color_counts[color[i]] += 1 + end + if !visited_vertices[j] + visited_vertices[j] = true + column_color_counts[color[j]] += 1 + end + end + end + end + end + end + end + + # Only trivial stars, where both vertices can be promoted as hubs, remain. + # In the context of bicoloring, if we aim to minimize either the number of row colors or the number of column colors, + # we can achieve optimal post-processing by choosing as hubs the vertices from the opposite partition. + # This is optimal because we never increase the number of colors in the target partition during this phase, + # and all preceding steps of the post-processing are deterministic. + if !all_trivial_stars_treated + rvS = rowvals(S) + for j in axes(S, 2) + for k in nzrange(S, j) + i = rvS[k] + if i > j + index_ij = edge_to_index[k] + s = star[index_ij] + h = hub[s] + # The hub of this trivial star is still unknown + if h < 0 + # We need to decide who is the hub + if !row_color_used[color[i]] && !column_color_used[color[j]] + if !bicoloring || postprocessing_minimizes == :all_colors + # Choose as hub the vertex whose color is most frequent among the trivial stars. + # Colors with smaller `color_counts` are easier to keep unused + # if their vertices remain spokes instead of hubs. + # This is an heuristic to try to reduce the number of colors used. + # + # In case of a tie, we prefer to preserve colors in the row partition. + # + # Note that this heuristic also depends on the order in which + # the trivial stars are processed, especially when there are ties in `color_counts`. + if row_color_counts[color[i]] > column_color_counts[color[j]] + row_color_used[color[i]] = true + hub[s] = i + else + column_color_used[color[j]] = true + hub[s] = j + end + elseif postprocessing_minimizes == :row_colors + # j belongs to a column partition in the context of bicoloring + hub[s] = j + column_color_used[color[j]] = true + elseif postprocessing_minimizes == :column_colors + # i belongs to a row partition in the context of bicoloring + hub[s] = i + row_color_used[color[i]] = true + else + error("The value postprocessing_minimizes = :$postprocessing_minimizes is not supported.") + end + else + # Previously processed trivial stars determined the hub vertex for this star + if row_color_used[color[i]] + hub[s] = i + else + hub[s] = j + end + end end end end end end end - return color_used + return nothing end function postprocess_with_tree_set!( - color_used::Vector{Bool}, + bicoloring::Bool, + row_color_used::Vector{Bool}, + column_color_used::Vector{Bool}, color::AbstractVector{<:Integer}, tree_set::TreeSet, + postprocessing_minimizes::Symbol=:all_colors, ) # only the colors of non-leaf vertices are used (; reverse_bfs_orders, is_star, tree_edge_indices, nt) = tree_set @@ -130,13 +235,19 @@ function postprocess_with_tree_set!( # Determine if the tree is a star if is_star[k] # It is a non-trivial star and only the color of the hub is needed - (_, hub) = reverse_bfs_orders[first] - color_used[color[hub]] = true + (leaf, hub) = reverse_bfs_orders[first] + if hub < leaf + column_color_used[color[hub]] = true + else + row_color_used[color[hub]] = true + end else # It is not a star and both colors are needed during the decompression (i, j) = reverse_bfs_orders[first] - color_used[color[i]] = true - color_used[color[j]] = true + v_col = min(i,j) + v_row = max(i,j) + row_color_used[color[v_row]] = true + column_color_used[color[v_col]] = true end else nb_trivial_trees += 1 @@ -145,6 +256,14 @@ function postprocess_with_tree_set!( # Process the trivial trees (if any) if nb_trivial_trees > 0 + # When bicoloring is false, row_color_counts and column_color_counts point to the same memory + nv = length(color) + nb_colors = length(row_color_used) + visited_vertices = zeros(Bool, nv) + row_color_counts = zeros(Int, nb_colors) + column_color_counts = bicoloring ? zeros(Int, nb_colors) : row_color_counts + all_trivial_trees_treated = true + for k in 1:nt # Position of the first edge in the tree first = tree_edge_indices[k] @@ -155,16 +274,93 @@ function postprocess_with_tree_set!( # Check if we have exactly one edge in the tree if ne_tree == 1 (i, j) = reverse_bfs_orders[first] - if color_used[color[i]] - # Make i the root to avoid possibly adding one more used color - # Switch it with the (only) leaf - reverse_bfs_orders[first] = (j, i) + v_col = min(i,j) + v_row = max(i,j) + if column_color_used[color[v_col]] + # The vertex v_col is already an internal node in a non-trivial tree + reverse_bfs_orders[first] = (v_row, v_col) else - # Keep j as the root - color_used[color[j]] = true + if row_color_used[color[v_row]] + # The vertex v_row is already an internal node in a non-trivial tree + reverse_bfs_orders[first] = (v_col, v_row) + else + all_trivial_trees_treated = false + # Count how many vertices of each color appear among the remaining trivial trees. + # Each vertex is counted at most once, using `visited_vertices` to avoid duplicates + # when a vertex belongs to multiple trivial trees. + if !visited_vertices[v_row] + visited_vertices[v_row] = true + row_color_counts[color[v_row]] += 1 + end + if !visited_vertices[v_col] + visited_vertices[v_col] = true + column_color_counts[color[v_col]] += 1 + end + end + end + end + end + + # Only trivial trees, where both vertices can be promoted as roots, remain. + # In the context of bicoloring, if we aim to minimize either the number of row colors or the number of column colors, + # we can achieve optimal post-processing by choosing as roots the vertices from the opposite partition. + # This is optimal because we never increase the number of colors in the target partition during this phase, + # and all preceding steps of the post-processing are deterministic. + if !all_trivial_trees_treated + for k in 1:nt + # Position of the first edge in the tree + first = tree_edge_indices[k] + + # Total number of edges in the tree + ne_tree = tree_edge_indices[k + 1] - first + + # Check if we have exactly one edge in the tree + if ne_tree == 1 + (i, j) = reverse_bfs_orders[first] + v_col = min(i,j) + v_row = max(i,j) + if !column_color_used[color[v_col]] && !row_color_used[color[v_row]] + if !bicoloring || postprocessing_minimizes == :all_colors + # Choose as root the vertex whose color is most frequent among the trivial trees. + # Colors with smaller `color_counts` are easier to keep unused + # if their vertices remain leaves instead of roots. + # This is an heuristic to try to reduce the number of colors used. + # + # In case of a tie, we prefer to preserve colors in the row partition. + # + # Note that this heuristic also depends on the order in which + # the trivial trees are processed, especially when there are ties in `color_counts`. + if row_color_counts[color[v_row]] > column_color_counts[color[v_col]] + row_color_used[color[v_row]] = true + reverse_bfs_orders[first] = (v_col, v_row) + else + column_color_used[color[v_col]] = true + reverse_bfs_orders[first] = (v_row, v_col) + end + elseif postprocessing_minimizes == :row_colors + # v_col belongs to a column partition in the context of bicoloring + column_color_used[color[v_col]] = true + reverse_bfs_orders[first] = (v_row, v_col) + elseif postprocessing_minimizes == :column_colors + # v_row belongs to a row partition in the context of bicoloring + row_color_used[color[v_row]] = true + reverse_bfs_orders[first] = (v_col, v_row) + else + error("The value postprocessing_minimizes = :$postprocessing_minimizes is not supported.") + end + else + # Previously processed trivial trees determined the root vertex for this tree + # Ensure that the root vertex has a used color for decompression + if column_color_used[color[v_col]] && !row_color_used[color[v_row]] + reverse_bfs_orders[first] = (v_row, v_col) + end + if !column_color_used[color[v_col]] && row_color_used[color[v_row]] + reverse_bfs_orders[first] = (v_col, v_row) + end + end end end end end - return color_used + return nothing end diff --git a/test/random.jl b/test/random.jl index 406dfea9..65ba636e 100644 --- a/test/random.jl +++ b/test/random.jl @@ -84,7 +84,13 @@ end; RandomOrder(StableRNG(0), 0); postprocessing=false, decompression=:direct ), GreedyColoringAlgorithm( - RandomOrder(StableRNG(0), 0); postprocessing=true, decompression=:direct + RandomOrder(StableRNG(0), 0); postprocessing=true, postprocessing_minimizes=:all_colors, decompression=:direct + ), + GreedyColoringAlgorithm( + RandomOrder(StableRNG(0), 0); postprocessing=true, postprocessing_minimizes=:row_colors, decompression=:direct + ), + GreedyColoringAlgorithm( + RandomOrder(StableRNG(0), 0); postprocessing=true, postprocessing_minimizes=:column_colors, decompression=:direct ), ) @testset "$((; m, n, p))" for (m, n, p) in asymmetric_params @@ -105,7 +111,13 @@ end; RandomOrder(StableRNG(0), 0); postprocessing=false, decompression=:substitution ), GreedyColoringAlgorithm( - RandomOrder(StableRNG(0), 0); postprocessing=true, decompression=:substitution + RandomOrder(StableRNG(0), 0); postprocessing=true, postprocessing_minimizes=:all_colors, decompression=:substitution + ), + GreedyColoringAlgorithm( + RandomOrder(StableRNG(0), 0); postprocessing=true, postprocessing_minimizes=:row_colors, decompression=:substitution + ), + GreedyColoringAlgorithm( + RandomOrder(StableRNG(0), 0); postprocessing=true, postprocessing_minimizes=:column_colors, decompression=:substitution ), ) @testset "$((; m, n, p))" for (m, n, p) in asymmetric_params diff --git a/test/small.jl b/test/small.jl index 2e12372c..c630ff2a 100644 --- a/test/small.jl +++ b/test/small.jl @@ -88,111 +88,137 @@ end; end; @testset "Bidirectional coloring" begin - problem = ColoringProblem(; structure=:nonsymmetric, partition=:bidirectional) - order = RandomOrder(StableRNG(0), 0) - - @testset "Anti-diagonal" begin - A = sparse([0 0 0 1; 0 0 1 0; 0 1 0 0; 1 0 0 0]) - - result = coloring( - A, problem, GreedyColoringAlgorithm{:direct}(; postprocessing=false) - ) - @test ncolors(result) == 2 - - result = coloring( - A, problem, GreedyColoringAlgorithm{:direct}(; postprocessing=true) - ) - @test ncolors(result) == 1 - - result = coloring( - A, problem, GreedyColoringAlgorithm{:substitution}(; postprocessing=false) - ) - @test ncolors(result) == 2 - - result = coloring( - A, problem, GreedyColoringAlgorithm{:substitution}(; postprocessing=true) - ) - @test ncolors(result) == 1 - end + color_stats = Dict("Anti-diagonal" => Dict(:direct => Dict(:all_colors => (0,0), :row_colors => (0,0), :column_colors => (0,0)), + :substitution => Dict(:all_colors => (0,0), :row_colors => (0,0), :column_colors => (0,0))), + "Triangle" => Dict(:direct => Dict(:all_colors => (0,0), :row_colors => (0,0), :column_colors => (0,0)), + :substitution => Dict(:all_colors => (0,0), :row_colors => (0,0), :column_colors => (0,0))), + "Rectangle" => Dict(:direct => Dict(:all_colors => (0,0), :row_colors => (0,0), :column_colors => (0,0)), + :substitution => Dict(:all_colors => (0,0), :row_colors => (0,0), :column_colors => (0,0))), + "Arrowhead" => Dict(:direct => Dict(:all_colors => (0,0), :row_colors => (0,0), :column_colors => (0,0)), + :substitution => Dict(:all_colors => (0,0), :row_colors => (0,0), :column_colors => (0,0)))) + + @testset "postprocessing_minimizes = $target" for target in (:all_colors, :row_colors, :column_colors) + problem = ColoringProblem(; structure=:nonsymmetric, partition=:bidirectional) + order = RandomOrder(StableRNG(0), 0) + + @testset "Anti-diagonal" begin + A = sparse([0 0 0 1; 0 0 1 0; 0 1 0 0; 1 0 0 0]) + + result = coloring( + A, problem, GreedyColoringAlgorithm{:direct}(; postprocessing=false), + ) + @test ncolors(result) == 2 + + result = coloring( + A, problem, GreedyColoringAlgorithm{:direct}(; postprocessing=true, postprocessing_minimizes=target), + ) + @test ncolors(result) == 1 + color_stats["Anti-diagonal"][:direct][target] = (row_colors(result) |> maximum, column_colors(result) |> maximum) + + result = coloring( + A, problem, GreedyColoringAlgorithm{:substitution}(; postprocessing=false), + ) + @test ncolors(result) == 2 + + result = coloring( + A, problem, GreedyColoringAlgorithm{:substitution}(; postprocessing=true, postprocessing_minimizes=target), + ) + @test ncolors(result) == 1 + color_stats["Anti-diagonal"][:substitution][target] = (row_colors(result) |> maximum, column_colors(result) |> maximum) + end - @testset "Triangle" begin - A = sparse([1 1 0; 0 1 1; 1 0 1]) + @testset "Triangle" begin + A = sparse([1 1 0; 0 1 1; 1 0 1]) - result = coloring( - A, problem, GreedyColoringAlgorithm{:direct}(; postprocessing=true) - ) - @test ncolors(result) == 3 + result = coloring( + A, problem, GreedyColoringAlgorithm{:direct}(; postprocessing=true, postprocessing_minimizes=target), + ) + @test ncolors(result) == 3 + color_stats["Triangle"][:direct][target] = (row_colors(result) |> maximum, column_colors(result) |> maximum) - result = coloring( - A, problem, GreedyColoringAlgorithm{:substitution}(; postprocessing=true) - ) - @test ncolors(result) == 3 - end - - @testset "Rectangle" begin - A = spzeros(Bool, 10, 20) - A[:, 1] .= 1 - A[:, end] .= 1 - A[1, :] .= 1 - A[end, :] .= 1 - - result = coloring( - A, problem, GreedyColoringAlgorithm{:direct}(order; postprocessing=false) - ) - @test ncolors(result) == 6 # two more than necessary - result = coloring( - A, problem, GreedyColoringAlgorithm{:direct}(order; postprocessing=true) - ) - @test ncolors(result) == 4 # optimal number - - result = coloring( - A, problem, GreedyColoringAlgorithm{:substitution}(order; postprocessing=false) - ) - @test ncolors(result) == 6 # two more than necessary - result = coloring( - A, problem, GreedyColoringAlgorithm{:substitution}(order; postprocessing=true) - ) - @test ncolors(result) == 4 # optimal number - end + result = coloring( + A, problem, GreedyColoringAlgorithm{:substitution}(; postprocessing=true, postprocessing_minimizes=target), + ) + @test ncolors(result) == 3 + color_stats["Triangle"][:substitution][target] = (row_colors(result) |> maximum, column_colors(result) |> maximum) + end - @testset "Arrowhead" begin - A = spzeros(Bool, 10, 10) - for i in axes(A, 1) - A[1, i] = 1 - A[i, 1] = 1 - A[i, i] = 1 + @testset "Rectangle" begin + A = spzeros(Bool, 10, 20) + A[:, 1] .= 1 + A[:, end] .= 1 + A[1, :] .= 1 + A[end, :] .= 1 + + result = coloring( + A, problem, GreedyColoringAlgorithm{:direct}(order; postprocessing=false), + ) + @test ncolors(result) == 6 # two more than necessary + result = coloring( + A, problem, GreedyColoringAlgorithm{:direct}(order; postprocessing=true, postprocessing_minimizes=target), + ) + @test ncolors(result) == 4 # optimal number + color_stats["Rectangle"][:direct][target] = (row_colors(result) |> maximum, column_colors(result) |> maximum) + + result = coloring( + A, problem, GreedyColoringAlgorithm{:substitution}(order; postprocessing=false), + ) + @test ncolors(result) == 6 # two more than necessary + result = coloring( + A, problem, GreedyColoringAlgorithm{:substitution}(order; postprocessing=true, postprocessing_minimizes=target), + ) + @test ncolors(result) == 4 # optimal number + color_stats["Rectangle"][:substitution][target] = (row_colors(result) |> maximum, column_colors(result) |> maximum) end - result = coloring( - A, problem, GreedyColoringAlgorithm{:direct}(order; postprocessing=true) - ) - @test ncolors(coloring(A, problem, GreedyColoringAlgorithm{:substitution}(order))) < - ncolors(coloring(A, problem, GreedyColoringAlgorithm{:direct}(order))) + @testset "Arrowhead" begin + A = spzeros(Bool, 10, 10) + for i in axes(A, 1) + A[1, i] = 1 + A[i, 1] = 1 + A[i, i] = 1 + end + + @test ncolors(coloring(A, problem, GreedyColoringAlgorithm{:substitution}(order))) < + ncolors(coloring(A, problem, GreedyColoringAlgorithm{:direct}(order))) + + result_direct = coloring( + A, problem, GreedyColoringAlgorithm{:direct}(order; postprocessing=true, postprocessing_minimizes=target), + ) + color_stats["Arrowhead"][:direct][target] = (row_colors(result_direct) |> maximum, column_colors(result_direct) |> maximum) + + result_substitution = coloring( + A, problem, GreedyColoringAlgorithm{:substitution}(order; postprocessing=true, postprocessing_minimizes=target), + ) + color_stats["Arrowhead"][:substitution][target] = (row_colors(result_substitution) |> maximum, column_colors(result_substitution) |> maximum) - @test ncolors( - coloring( + @test ncolors(result_substitution) < ncolors(result_direct) + + test_bicoloring_decompression( + A, + problem, + GreedyColoringAlgorithm{:direct}(order; postprocessing=true, postprocessing_minimizes=target); + test_fast=true, + ) + + test_bicoloring_decompression( A, problem, - GreedyColoringAlgorithm{:substitution}(order; postprocessing=true), - ), - ) < ncolors( - coloring( - A, problem, GreedyColoringAlgorithm{:direct}(order; postprocessing=true) - ), - ) - - test_bicoloring_decompression( - A, - problem, - GreedyColoringAlgorithm{:direct}(order; postprocessing=true); - test_fast=true, - ) - - test_bicoloring_decompression( - A, - problem, - GreedyColoringAlgorithm{:substitution}(order; postprocessing=true); - test_fast=true, - ) + GreedyColoringAlgorithm{:substitution}(order; postprocessing=true, postprocessing_minimizes=target); + test_fast=true, + ) + end + end + + @testset "Variants of post-processing" begin + for problem in ("Anti-diagonal", "Triangle", "Rectangle", "Arrowhead") + for mode in (:direct, :substitution) + (num_row_colors1, num_column_colors1) = color_stats[problem][mode][:row_colors] + (num_row_colors2, num_column_colors2) = color_stats[problem][mode][:all_colors] + (num_row_colors3, num_column_colors3) = color_stats[problem][mode][:column_colors] + @test num_row_colors1 ≤ num_row_colors2 ≤ num_row_colors3 + @test num_column_colors3 ≤ num_column_colors2 ≤ num_column_colors1 + end + end end end; diff --git a/test/utils.jl b/test/utils.jl index 2462f8c1..b5e56292 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -240,10 +240,10 @@ function test_bicoloring_decompression( @testset "More orders is better" begin more_orders = (algo.orders..., _ALL_ORDERS...) better_algo = GreedyColoringAlgorithm{decompression}( - more_orders; algo.postprocessing + more_orders; algo.postprocessing, algo.postprocessing_minimizes, ) all_algos = [ - GreedyColoringAlgorithm{decompression}(order; algo.postprocessing) for + GreedyColoringAlgorithm{decompression}(order; algo.postprocessing, algo.postprocessing_minimizes) for order in more_orders ] result = coloring(A0, problem, algo) @@ -267,10 +267,6 @@ function test_structured_coloring_decompression(A::AbstractMatrix) @test D == A @test nameof(typeof(D)) == nameof(typeof(A)) @test structurally_orthogonal_columns(A, color) - if VERSION >= v"1.10" || A isa Union{Diagonal,Bidiagonal,Tridiagonal} - # banded matrices not supported by ArrayInterface on Julia 1.6 - # @test color == ArrayInterface.matrix_colors(A) # TODO: uncomment - end # Row result = coloring(A, row_problem, algo)