diff --git a/docs/src/dev.md b/docs/src/dev.md index 84234f5..d60ce8d 100644 --- a/docs/src/dev.md +++ b/docs/src/dev.md @@ -48,6 +48,7 @@ SparseMatrixColorings.remap_colors SparseMatrixColorings.directly_recoverable_columns SparseMatrixColorings.symmetrically_orthogonal_columns SparseMatrixColorings.structurally_orthogonal_columns +SparseMatrixColorings.structurally_biorthogonal SparseMatrixColorings.valid_dynamic_order ``` diff --git a/src/check.jl b/src/check.jl index e30619b..5210a3f 100644 --- a/src/check.jl +++ b/src/check.jl @@ -1,18 +1,42 @@ function proper_length_coloring( - A::AbstractMatrix, color::AbstractVector{<:Integer}; verbose::Bool + A::AbstractMatrix, color::AbstractVector{<:Integer}; verbose::Bool=false ) - if length(color) != size(A, 2) + m, n = size(A) + if length(color) != n if verbose - @warn "$(length(color)) colors provided for $(size(A, 2)) columns." + @warn "$(length(color)) colors provided for $n columns." end return false end return true end +function proper_length_bicoloring( + A::AbstractMatrix, + row_color::AbstractVector{<:Integer}, + column_color::AbstractVector{<:Integer}; + verbose::Bool=false, +) + m, n = size(A) + bool = true + if length(row_color) != m + if verbose + @warn "$(length(row_color)) colors provided for $m rows." + end + bool = false + end + if length(column_color) != n + if verbose + @warn "$(length(column_color)) colors provided for $n columns." + end + bool = false + end + return bool +end + """ structurally_orthogonal_columns( - A::AbstractMatrix, color::AbstractVector{<:Integer} + A::AbstractMatrix, color::AbstractVector{<:Integer}; verbose=false ) @@ -56,8 +80,8 @@ end ) Return `true` if coloring the columns of the symmetric matrix `A` with the vector `color` results in a partition that is symmetrically orthogonal, and `false` otherwise. - -A partition of the columns of a symmetrix matrix `A` is _symmetrically orthogonal_ if, for every nonzero element `A[i, j]`, either of the following statements holds: + +A partition of the columns of a symmetric matrix `A` is _symmetrically orthogonal_ if, for every nonzero element `A[i, j]`, either of the following statements holds: 1. the group containing the column `A[:, j]` has no other column with a nonzero in row `i` 2. the group containing the column `A[:, i]` has no other column with a nonzero in row `j` @@ -102,6 +126,57 @@ function symmetrically_orthogonal_columns( return true end +""" + structurally_biorthogonal( + A::AbstractMatrix, row_color::AbstractVector{<:Integer}, column_color::AbstractVector{<:Integer}; + verbose=false + ) + +Return `true` if bicoloring of the matrix `A` with the vectors `row_color` and `column_color` results in a bipartition that is structurally biorthogonal, and `false` otherwise. + +A bipartition of the rows and columns of a matrix `A` is _structurally biorthogonal_ if, for every nonzero element `A[i, j]`, either of the following statements holds: + +1. the group containing the column `A[:, j]` has no other column with a nonzero in row `i` +2. the group containing the row `A[i, :]` has no other row with a nonzero in column `j` + +!!! warning + This function is not coded with efficiency in mind, it is designed for small-scale tests. +""" +function structurally_biorthogonal( + A::AbstractMatrix, + row_color::AbstractVector{<:Integer}, + column_color::AbstractVector{<:Integer}; + verbose::Bool=false, +) + if !proper_length_bicoloring(A, row_color, column_color; verbose) + return false + end + column_group = group_by_color(column_color) + row_group = group_by_color(row_color) + for i in axes(A, 1), j in axes(A, 2) + iszero(A[i, j]) && continue + ci, cj = row_color[i], column_color[j] + gi, gj = row_group[ci], column_group[cj] + A_gj_rowi = view(A, i, gj) + A_gi_columnj = view(A, gi, j) + nonzeros_gj_rowi = count(!iszero, A_gj_rowi) + nonzeros_gi_columnj = count(!iszero, A_gi_columnj) + if nonzeros_gj_rowi > 1 && nonzeros_gi_columnj > 1 + if verbose + gj_incompatible_columns = gj[findall(!iszero, A_gj_rowi)] + gi_incompatible_rows = gi[findall(!iszero, A_gi_columnj)] + @warn """ + For coefficient (i=$i, j=$j) with row color ci=$ci and column color cj=$cj: + - In row color ci=$ci, rows $gi_incompatible_rows all have nonzeros in column j=$j. + - In column color cj=$cj, columns $gj_incompatible_columns all have nonzeros in row i=$i. + """ + end + return false + end + end + return true +end + """ directly_recoverable_columns( A::AbstractMatrix, color::AbstractVector{<:Integer} diff --git a/src/graph.jl b/src/graph.jl index f4af4ae..d491f10 100644 --- a/src/graph.jl +++ b/src/graph.jl @@ -100,7 +100,7 @@ end Undirected graph without self-loops representing the nonzeros of a symmetric matrix (typically a Hessian matrix). -The adjacency graph of a symmetrix matric `A ∈ ℝ^{n × n}` is `G(A) = (V, E)` where +The adjacency graph of a symmetric matrix `A ∈ ℝ^{n × n}` is `G(A) = (V, E)` where - `V = 1:n` is the set of rows or columns `i`/`j` - `(i, j) ∈ E` whenever `A[i, j] ≠ 0` and `i ≠ j` diff --git a/test/check.jl b/test/check.jl index a90b4af..60e7ecc 100644 --- a/test/check.jl +++ b/test/check.jl @@ -2,6 +2,7 @@ using LinearAlgebra using SparseMatrixColorings: structurally_orthogonal_columns, symmetrically_orthogonal_columns, + structurally_biorthogonal, directly_recoverable_columns, what_fig_41, efficient_fig_1 @@ -122,3 +123,37 @@ For coefficient (i=2, j=3) with column colors (ci=3, cj=1): @test !directly_recoverable_columns(A, [1, 2, 1, 3, 1, 4, 2, 5, 1, 2]) @test !directly_recoverable_columns(A, [1, 2, 1, 4, 1, 4, 3, 5, 1, 2]) end + +@testset "Structurally biorthogonal" begin + A = [ + 1 5 7 9 11 + 2 0 0 0 12 + 3 0 0 0 13 + 4 6 8 10 14 + ] + + # success + + @test structurally_biorthogonal(A, [1, 2, 2, 3], [1, 2, 2, 2, 3]) + + # failure + + @test !structurally_biorthogonal(A, [1, 2, 2, 3], [1, 2, 2, 2]) + @test !structurally_biorthogonal(A, [1, 2, 2, 3, 4], [1, 2, 2, 2, 3]) + @test !structurally_biorthogonal(A, [1, 1, 1, 2], [1, 1, 1, 1, 2]) + + @test_logs (:warn, "4 colors provided for 5 columns.") !structurally_biorthogonal( + A, [1, 2, 2, 3], [1, 2, 2, 2]; verbose=true + ) + @test_logs (:warn, "5 colors provided for 4 rows.") !structurally_biorthogonal( + A, [1, 2, 2, 3, 4], [1, 2, 2, 2, 3]; verbose=true + ) + @test_logs ( + :warn, + """ +For coefficient (i=1, j=1) with row color ci=1 and column color cj=1: +- In row color ci=1, rows [1, 2, 3] all have nonzeros in column j=1. +- In column color cj=1, columns [1, 2, 3, 4] all have nonzeros in row i=1. +""", + ) !structurally_biorthogonal(A, [1, 1, 1, 2], [1, 1, 1, 1, 2]; verbose=true) +end diff --git a/test/utils.jl b/test/utils.jl index de76624..d58461c 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -10,7 +10,8 @@ using SparseMatrixColorings: matrix_versions, respectful_similar, structurally_orthogonal_columns, - symmetrically_orthogonal_columns + symmetrically_orthogonal_columns, + structurally_biorthogonal using Test function test_coloring_decompression( @@ -59,13 +60,16 @@ function test_coloring_decompression( if partition == :column @test structurally_orthogonal_columns(A0, color) @test directly_recoverable_columns(A0, color) - else + elseif partition == :row @test structurally_orthogonal_columns(transpose(A0), color) @test directly_recoverable_columns(transpose(A0), color) end else - @test symmetrically_orthogonal_columns(A0, color) - @test directly_recoverable_columns(A0, color) + # structure == :symmetric + if partition == :column + @test symmetrically_orthogonal_columns(A0, color) + @test directly_recoverable_columns(A0, color) + end end end end @@ -163,9 +167,16 @@ function test_bicoloring_decompression( result = coloring(A, problem, algo; decompression_eltype=Float64) end Br, Bc = compress(A, result) - @test size(Br, 1) == length(unique(row_colors(result))) - @test size(Bc, 2) == length(unique(column_colors(result))) + row_color, column_color = row_colors(result), column_colors(result) + @test size(Br, 1) == length(unique(row_color)) + @test size(Bc, 2) == length(unique(column_color)) @test ncolors(result) == size(Br, 1) + size(Bc, 2) + + if decompression == :direct + @testset "Recoverability" begin + @test structurally_biorthogonal(A0, row_color, column_color) + end + end @testset "Full decompression" begin @test decompress(Br, Bc, result) ≈ A0 @test decompress(Br, Bc, result) ≈ A0 # check result wasn't modified