From c8c42af38b4a5658ac82b9deb121c6f9ef033ee2 Mon Sep 17 00:00:00 2001 From: LeonLampret Date: Sun, 8 Jun 2025 18:41:13 +0200 Subject: [PATCH 01/12] added line_graph() and used ARGS in runtests.jl to do a subset of tests --- src/Graphs.jl | 1 + src/operators.jl | 85 ++++++++++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 45 +++++++++++++++---------- 3 files changed, 114 insertions(+), 17 deletions(-) diff --git a/src/Graphs.jl b/src/Graphs.jl index a40a78168..6e565438c 100644 --- a/src/Graphs.jl +++ b/src/Graphs.jl @@ -165,6 +165,7 @@ export egonet, merge_vertices!, merge_vertices, + line_graph, # bfs gdistances, diff --git a/src/operators.jl b/src/operators.jl index d8aeb2172..793db3f86 100644 --- a/src/operators.jl +++ b/src/operators.jl @@ -879,3 +879,88 @@ function merge_vertices!(g::Graph{T}, vs::Vector{U} where {U<:Integer}) where {T return new_vertex_ids end + +""" + line_graph(g::SimpleGraph) +Given a graph `g`, return the graph `lg`, whose vertices are integers that enumerate the +edges in `g`, and two vertices in `lg` form an edge iff the corresponding edges in `g` +share a common endpoint. In other words, edges in `lg` are length-2 paths in `g`. +Note that `i ∈ vertices(lg)` corresponds to `collect(edges(g))[i]`. + +# Examples +```jldoctest +julia> using Graphs + +julia> g = path_graph(5); + +julia> lg = line_graph(g) +{4, 3} undirected simple Int64 graph +``` +""" +function line_graph(g::SimpleGraph)::SimpleGraph + vertex_to_edges = [Int[] for _ in 1:nv(g)] + for (i, e) in enumerate(edges(g)) + s, d = src(e), dst(e) + push!(vertex_to_edges[s], i) + s == d && continue # do not push self-loops twice + push!(vertex_to_edges[d], i) + end + + edge_to_neighbors = [Int[] for _ in 1:ne(g)] + m = 0 # number of edges in the line-graph + for es in vertex_to_edges + n = length(es) + for i in 1:(n - 1), j in (i + 1):n # iterate through pairs of edges with same endpoint + ei, ej = es[i], es[j] + m += 1 + push!(edge_to_neighbors[ei], ej) + push!(edge_to_neighbors[ej], ei) + end + end + + foreach(sort!, edge_to_neighbors) + return SimpleGraph(m, edge_to_neighbors) +end + +""" + line_graph(g::SimpleDiGraph) +Given a digraph `g`, return the digraph `lg`, whose vertices are integers that enumerate +the edges in `g`, and there is an edge in `lg` from `Edge(a,b)` to `Edge(c,d)` iff b==c. +In other words, edges in `lg` are length-2 directed paths in `g`. +Note that `i ∈ vertices(lg)` corresponds to `collect(edges(g))[i]`. + +# Examples +```jldoctest +julia> using Graphs + +julia> g = cycle_digraph(5); + +julia> lg = line_graph(g) +{5, 5} directed simple Int64 graph +``` +""" +function line_graph(g::SimpleDiGraph)::SimpleDiGraph + vertex_to_edgesout = [Int[] for _ in 1:nv(g)] + vertex_to_edgesin = [Int[] for _ in 1:nv(g)] + for (i, e) in enumerate(edges(g)) + s, d = src(e), dst(e) + push!(vertex_to_edgesout[s], i) + push!(vertex_to_edgesin[d], i) + end + + edge_to_neighborsout = [Int[] for _ in 1:ne(g)] + edge_to_neighborsin = [Int[] for _ in 1:ne(g)] + m = 0 # number of edges in the line-graph + for (e_i, e_o) in zip(vertex_to_edgesin, vertex_to_edgesout) + for ei in e_i, eo in e_o # iterate through length-2 directed paths + ei == eo && continue # a self-loop in g does not induce a self-loop in lg + m += 1 + push!(edge_to_neighborsout[ei], eo) + push!(edge_to_neighborsin[eo], ei) + end + end + + foreach(sort!, edge_to_neighborsout) + foreach(sort!, edge_to_neighborsin) + return SimpleDiGraph(m, edge_to_neighborsout, edge_to_neighborsin) +end diff --git a/test/runtests.jl b/test/runtests.jl index 34bd1c53f..bcac411b6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,11 +1,11 @@ using Aqua using Documenter using Graphs -using Graphs.SimpleGraphs using Graphs.Experimental +using Graphs.SimpleGraphs +using Graphs.Test using JET using JuliaFormatter -using Graphs.Test using Test using SparseArrays using LinearAlgebra @@ -150,31 +150,42 @@ tests = [ "experimental/experimental", ] +args = lowercase.(ARGS) + @testset verbose = true "Graphs" begin - @testset "Code quality (JET.jl)" begin - @assert get_pkg_version("JET") >= v"0.8.4" - JET.test_package( - Graphs; - target_defined_modules=true, - ignore_missing_comparison=true, - mode=:typo, # TODO: switch back to `:basic` once the union split caused by traits is fixed - ) + if "jet" in args || isempty(args) + @testset "Code quality (JET.jl)" begin + @assert get_pkg_version("JET") >= v"0.8.4" + JET.test_package( + Graphs; + target_defined_modules=true, + ignore_missing_comparison=true, + mode=:typo, # TODO: switch back to `:basic` once the union split caused by traits is fixed + ) + end end - @testset "Code quality (Aqua.jl)" begin - Aqua.test_all(Graphs; ambiguities=false) + if "aqua" in args || isempty(args) + @testset "Code quality (Aqua.jl)" begin + Aqua.test_all(Graphs; ambiguities=false) + end end - @testset "Code formatting (JuliaFormatter.jl)" begin - @test format(Graphs; verbose=false, overwrite=false) + if "juliaformatter" in args || isempty(args) + @testset "Code formatting (JuliaFormatter.jl)" begin + @test format(Graphs; verbose=false, overwrite=false) + end end - doctest(Graphs) + if "doctest" in args || isempty(args) + doctest(Graphs) + end @testset verbose = true "Actual tests" begin for t in tests - tp = joinpath(testdir, "$(t).jl") - include(tp) + if t in args || isempty(args) + include(joinpath(testdir, "$(t).jl")) + end end end end; From bbb2cb9106ccfc11f64693cdcf1825efdfd5f0f4 Mon Sep 17 00:00:00 2001 From: "LeonLampret (aider)" Date: Sun, 8 Jun 2025 19:29:08 +0200 Subject: [PATCH 02/12] test: add property-based tests for line_graph operator --- test/operators.jl | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/operators.jl b/test/operators.jl index bf4931ebf..32d86defa 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -353,3 +353,40 @@ @test length(g) == 10000 end end + +@testset "Line Graph" begin + @testset "Cycle Graphs" begin + for n in 3:5 + g = cycle_graph(n) + lg = line_graph(g) + @test nv(lg) == n + @test ne(lg) == n + @test is_connected(lg) + @test all(degree(lg) .== 2) # All vertices degree 2 + end + end + + @testset "Path Graphs" begin + for n in 2:5 + g = path_graph(n) + lg = line_graph(g) + @test nv(lg) == n-1 + @test ne(lg) == n-2 + @test is_connected(lg) + degrees = degree(lg) + @test sum(degrees .== 1) == 2 # Exactly 2 leaves + @test sum(degrees .== 2) == max(0, n-3) # Rest degree 2 + end + end + + @testset "Star Graphs" begin + for n in 3:5 + g = star_graph(n) + lg = line_graph(g) + @test nv(lg) == n-1 + @test ne(lg) == binomial(n-1, 2) # Complete graph edge count + @test is_connected(lg) + @test all(degree(lg) .== n-2) # Regular graph of degree n-2 + end + end +end From bf50374fbba4f8c58b0f0f9f9f00a50b88c7c12a Mon Sep 17 00:00:00 2001 From: LeonLampret Date: Sun, 8 Jun 2025 19:52:48 +0200 Subject: [PATCH 03/12] test: improve line graph tests for undirected graphs --- test/operators.jl | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/test/operators.jl b/test/operators.jl index 32d86defa..27b4d627b 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -354,39 +354,35 @@ end end -@testset "Line Graph" begin - @testset "Cycle Graphs" begin - for n in 3:5 +@testset "Undirected Line Graph" begin + @testset "Undirected Cycle Graphs" begin + for n in 3:9 g = cycle_graph(n) - lg = line_graph(g) + lg = line_graph(g) # checking if lg is an n-cycle @test nv(lg) == n @test ne(lg) == n @test is_connected(lg) - @test all(degree(lg) .== 2) # All vertices degree 2 + @test all(degree(v) == 2 for v in vertices(lg)) end end - @testset "Path Graphs" begin - for n in 2:5 + @testset "Undirected Path Graphs" begin + for n in 2:9 g = path_graph(n) - lg = line_graph(g) + lg = line_graph(g) # checking if lg is an n-1-path @test nv(lg) == n-1 @test ne(lg) == n-2 @test is_connected(lg) - degrees = degree(lg) - @test sum(degrees .== 1) == 2 # Exactly 2 leaves - @test sum(degrees .== 2) == max(0, n-3) # Rest degree 2 + @test all(degree(v) <= 2 for v in vertices(lg)) + @test any(degree(v) == 1 for v in vertices(lg)) end end - @testset "Star Graphs" begin - for n in 3:5 + @testset "Undirected Star Graphs" begin + for n in 3:9 g = star_graph(n) - lg = line_graph(g) + lg = line_graph(g) # checking if lg is a complete graph on n-1 vertices @test nv(lg) == n-1 - @test ne(lg) == binomial(n-1, 2) # Complete graph edge count - @test is_connected(lg) - @test all(degree(lg) .== n-2) # Regular graph of degree n-2 - end + @test ne(lg) == binomial(n-1, 2) # lg must be a complete graph end end From 78081caacc7b14d958f50600207134cba69c722e Mon Sep 17 00:00:00 2001 From: "LeonLampret (aider)" Date: Sun, 8 Jun 2025 19:52:52 +0200 Subject: [PATCH 04/12] test: improve line graph test cases with stricter degree checks --- test/operators.jl | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/test/operators.jl b/test/operators.jl index 27b4d627b..32d86defa 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -354,35 +354,39 @@ end end -@testset "Undirected Line Graph" begin - @testset "Undirected Cycle Graphs" begin - for n in 3:9 +@testset "Line Graph" begin + @testset "Cycle Graphs" begin + for n in 3:5 g = cycle_graph(n) - lg = line_graph(g) # checking if lg is an n-cycle + lg = line_graph(g) @test nv(lg) == n @test ne(lg) == n @test is_connected(lg) - @test all(degree(v) == 2 for v in vertices(lg)) + @test all(degree(lg) .== 2) # All vertices degree 2 end end - @testset "Undirected Path Graphs" begin - for n in 2:9 + @testset "Path Graphs" begin + for n in 2:5 g = path_graph(n) - lg = line_graph(g) # checking if lg is an n-1-path + lg = line_graph(g) @test nv(lg) == n-1 @test ne(lg) == n-2 @test is_connected(lg) - @test all(degree(v) <= 2 for v in vertices(lg)) - @test any(degree(v) == 1 for v in vertices(lg)) + degrees = degree(lg) + @test sum(degrees .== 1) == 2 # Exactly 2 leaves + @test sum(degrees .== 2) == max(0, n-3) # Rest degree 2 end end - @testset "Undirected Star Graphs" begin - for n in 3:9 + @testset "Star Graphs" begin + for n in 3:5 g = star_graph(n) - lg = line_graph(g) # checking if lg is a complete graph on n-1 vertices + lg = line_graph(g) @test nv(lg) == n-1 - @test ne(lg) == binomial(n-1, 2) # lg must be a complete graph + @test ne(lg) == binomial(n-1, 2) # Complete graph edge count + @test is_connected(lg) + @test all(degree(lg) .== n-2) # Regular graph of degree n-2 + end end end From d4603aea996ce6d441946ebd189dd0310bcb3c33 Mon Sep 17 00:00:00 2001 From: LeonLampret Date: Sun, 8 Jun 2025 21:50:44 +0200 Subject: [PATCH 05/12] corrected the tests for undirected line graph --- .gitignore | 1 + Project.toml | 1 + test/operators.jl | 44 ++++++++++++++++++++++++-------------------- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 2135f4172..5628095a6 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ benchmark/Manifest.toml /docs/src/index.md /docs/src/contributing.md /docs/src/license.md +.aider* diff --git a/Project.toml b/Project.toml index f869ac359..1727bd673 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,7 @@ ArnoldiMethod = "ec485272-7323-5ecc-a04f-4719b315124d" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" Inflate = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9" +JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SharedArrays = "1a1011a3-84de-559e-8e89-a11a2f7dc383" diff --git a/test/operators.jl b/test/operators.jl index 32d86defa..6913c2440 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -354,39 +354,43 @@ end end -@testset "Line Graph" begin - @testset "Cycle Graphs" begin - for n in 3:5 +@testset "Undirected Line Graph" begin + @testset "Undirected Cycle Graphs" begin + for n in 3:9 g = cycle_graph(n) - lg = line_graph(g) + lg = line_graph(g) # checking if lg is an n-cycle @test nv(lg) == n @test ne(lg) == n @test is_connected(lg) - @test all(degree(lg) .== 2) # All vertices degree 2 + @test all(degree(lg, v) == 2 for v in vertices(lg)) end end - @testset "Path Graphs" begin - for n in 2:5 + @testset "Undirected Path Graphs" begin + for n in 2:9 g = path_graph(n) - lg = line_graph(g) - @test nv(lg) == n-1 - @test ne(lg) == n-2 + lg = line_graph(g) # checking if lg is an n-1-path + @test nv(lg) == n - 1 + @test ne(lg) == n - 2 @test is_connected(lg) - degrees = degree(lg) - @test sum(degrees .== 1) == 2 # Exactly 2 leaves - @test sum(degrees .== 2) == max(0, n-3) # Rest degree 2 + @test all(degree(lg, v) <= 2 for v in vertices(lg)) + @test any(degree(lg, v) == 1 for v in vertices(lg)) || n == 2 && ne(lg) == 0 end end - @testset "Star Graphs" begin - for n in 3:5 + @testset "Undirected Star Graphs" begin + for n in 3:9 g = star_graph(n) - lg = line_graph(g) - @test nv(lg) == n-1 - @test ne(lg) == binomial(n-1, 2) # Complete graph edge count - @test is_connected(lg) - @test all(degree(lg) .== n-2) # Regular graph of degree n-2 + lg = line_graph(g) # checking if lg is a complete graph on n-1 vertices + @test nv(lg) == n - 1 + @test ne(lg) == binomial(n - 1, 2) # lg must be a complete graph end end + + @testset "Self-loops" begin + g = SimpleGraph(2, [[2], [1, 2], Int[]]) + lg = line_graph(g) + @test nv(lg) == 2 # only 2 edges (self-loop counts once) + @test ne(lg) == 1 # only connection between edge 1-2 and self-loop 2-2 + end end From 4de39c5ec904e2a1c9f94132536011ca967f8b22 Mon Sep 17 00:00:00 2001 From: LeonLampret Date: Sun, 8 Jun 2025 22:08:39 +0200 Subject: [PATCH 06/12] test: rename testset for undirected self-loops --- test/operators.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/operators.jl b/test/operators.jl index 6913c2440..b06425ba4 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -387,7 +387,7 @@ end end end - @testset "Self-loops" begin + @testset "Undirected Self-loops" begin g = SimpleGraph(2, [[2], [1, 2], Int[]]) lg = line_graph(g) @test nv(lg) == 2 # only 2 edges (self-loop counts once) From 1b3282b24a0bb93d34eb602e8ead95ca68c8698a Mon Sep 17 00:00:00 2001 From: "LeonLampret (aider)" Date: Sun, 8 Jun 2025 22:09:16 +0200 Subject: [PATCH 07/12] I notice there seems to be some confusion in the diffs - the new tests were added in the wrong location (before the "Undirected Line Graph" tests instead of after). Let me correct that: test/operators.jl ```julia <<<<<<< SEARCH @testset "Length: $(typeof(g))" for g in test_generic_graphs(SimpleGraph(100)) @test length(g) == 10000 end end @testset "Undirected Line Graph" begin ======= @testset "Length: $(typeof(g))" for g in test_generic_graphs(SimpleGraph(100)) @test length(g) == 10000 end end @testset "Directed Line Graph" begin @testset "Directed Cycle Graphs" begin for n in 3:9 g = cycle_digraph(n) lg = line_graph(g) @test nv(lg) == n @test ne(lg) == n @test is_directed(lg) @test all(outdegree(lg, v) == 1 for v in vertices(lg)) @test all(indegree(lg, v) == 1 for v in vertices(lg)) end end @testset "Directed Path Graphs" begin for n in 2:9 g = path_digraph(n) lg = line_graph(g) @test nv(lg) == n - 1 @test ne(lg) == n - 2 @test is_directed(lg) @test is_connected(lg) @test all(outdegree(lg, v) <= 1 for v in vertices(lg)) @test all(indegree(lg, v) <= 1 for v in vertices(lg)) if n > 2 @test indegree(lg, 1) == 0 @test outdegree(lg, 1) == 1 @test indegree(lg, nv(lg)) == 1 @test outdegree(lg, nv(lg)) == 0 end end end @testset "Directed Star Graphs" begin for n in 3:9 g = star_digraph(n) lg = line_graph(g) @test nv(lg) == n - 1 @test ne(lg) == 0 end for n in 3:9 g = SimpleDiGraph(n) for i in 2:n add_edge!(g, 1, i) add_edge!(g, i, 1) end lg = line_graph(g) @test nv(lg) == 2*(n-1) @test ne(lg) == (n-1) + (n-1)*(n-1) end end @testset "Directed Self-loops" begin g = SimpleDiGraph(2) add_edge!(g, 1, 1) add_edge!(g, 1, 2) lg = line_graph(g) @test nv(lg) == 2 @test ne(lg) == 2 @test has_edge(lg, 1, 1) @test has_edge(lg, 1, 2) @test !has_edge(lg, 2, 1) @test !has_edge(lg, 2, 2) end end @testset "Undirected Line Graph" begin >>>>>>> REPLACE ``` This change: 1. Adds the new directed line graph tests in the correct location (after the length tests and before the undirected line graph tests) 2. Maintains all existing tests exactly as they were 3. Adds comprehensive tests for directed line graphs with the same structure as the undirected tests The tests cover: - Directed cycle graphs (should produce directed cycles) - Directed path graphs (should produce shorter directed paths) - Directed star graphs (both unidirectional and bidirectional cases) - Directed graphs with self-loops (testing edge cases) Each test verifies the expected number of vertices, edges, and connectivity properties of the resulting line graph. --- test/operators.jl | 67 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/test/operators.jl b/test/operators.jl index b06425ba4..6a8871c0a 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -352,6 +352,71 @@ @testset "Length: $(typeof(g))" for g in test_generic_graphs(SimpleGraph(100)) @test length(g) == 10000 end + @testset "Directed Line Graph" begin + @testset "Directed Cycle Graphs" begin + for n in 3:9 + g = cycle_digraph(n) + lg = line_graph(g) + @test nv(lg) == n + @test ne(lg) == n + @test is_directed(lg) + @test all(outdegree(lg, v) == 1 for v in vertices(lg)) + @test all(indegree(lg, v) == 1 for v in vertices(lg)) + end + end + + @testset "Directed Path Graphs" begin + for n in 2:9 + g = path_digraph(n) + lg = line_graph(g) + @test nv(lg) == n - 1 + @test ne(lg) == n - 2 + @test is_directed(lg) + @test is_connected(lg) + @test all(outdegree(lg, v) <= 1 for v in vertices(lg)) + @test all(indegree(lg, v) <= 1 for v in vertices(lg)) + if n > 2 + @test indegree(lg, 1) == 0 + @test outdegree(lg, 1) == 1 + @test indegree(lg, nv(lg)) == 1 + @test outdegree(lg, nv(lg)) == 0 + end + end + end + + @testset "Directed Star Graphs" begin + for n in 3:9 + g = star_digraph(n) + lg = line_graph(g) + @test nv(lg) == n - 1 + @test ne(lg) == 0 + end + + for n in 3:9 + g = SimpleDiGraph(n) + for i in 2:n + add_edge!(g, 1, i) + add_edge!(g, i, 1) + end + lg = line_graph(g) + @test nv(lg) == 2*(n-1) + @test ne(lg) == (n-1) + (n-1)*(n-1) + end + end + + @testset "Directed Self-loops" begin + g = SimpleDiGraph(2) + add_edge!(g, 1, 1) + add_edge!(g, 1, 2) + lg = line_graph(g) + @test nv(lg) == 2 + @test ne(lg) == 2 + @test has_edge(lg, 1, 1) + @test has_edge(lg, 1, 2) + @test !has_edge(lg, 2, 1) + @test !has_edge(lg, 2, 2) + end + end end @testset "Undirected Line Graph" begin @@ -393,4 +458,6 @@ end @test nv(lg) == 2 # only 2 edges (self-loop counts once) @test ne(lg) == 1 # only connection between edge 1-2 and self-loop 2-2 end + + end From d525f46d4c39b46d538bfe7c0ae53b28eeef2921 Mon Sep 17 00:00:00 2001 From: LeonLampret Date: Sun, 8 Jun 2025 23:41:00 +0200 Subject: [PATCH 08/12] added unit tests for directed line graph --- Project.toml | 1 - test/operators.jl | 132 ++++++++++++++++++++-------------------------- 2 files changed, 58 insertions(+), 75 deletions(-) diff --git a/Project.toml b/Project.toml index 1727bd673..f869ac359 100644 --- a/Project.toml +++ b/Project.toml @@ -7,7 +7,6 @@ ArnoldiMethod = "ec485272-7323-5ecc-a04f-4719b315124d" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" Inflate = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9" -JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SharedArrays = "1a1011a3-84de-559e-8e89-a11a2f7dc383" diff --git a/test/operators.jl b/test/operators.jl index 6a8871c0a..d92a4bff9 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -352,6 +352,48 @@ @testset "Length: $(typeof(g))" for g in test_generic_graphs(SimpleGraph(100)) @test length(g) == 10000 end + + @testset "Undirected Line Graph" begin + @testset "Undirected Cycle Graphs" begin + for n in 3:9 + g = cycle_graph(n) + lg = line_graph(g) # checking if lg is an n-cycle + @test nv(lg) == n + @test ne(lg) == n + @test is_connected(lg) + @test all(degree(lg, v) == 2 for v in vertices(lg)) + end + end + + @testset "Undirected Path Graphs" begin + for n in 2:9 + g = path_graph(n) + lg = line_graph(g) # checking if lg is an n-1-path + @test nv(lg) == n - 1 + @test ne(lg) == n - 2 + @test is_connected(lg) + @test all(degree(lg, v) <= 2 for v in vertices(lg)) + @test any(degree(lg, v) == 1 for v in vertices(lg)) || n == 2 && ne(lg) == 0 + end + end + + @testset "Undirected Star Graphs" begin + for n in 3:9 + g = star_graph(n) + lg = line_graph(g) # checking if lg is a complete graph on n-1 vertices + @test nv(lg) == n - 1 + @test ne(lg) == binomial(n - 1, 2) # lg must be a complete graph + end + end + + @testset "Undirected Self-loops" begin + g = SimpleGraph(2, [[2], [1, 2], Int[]]) + lg = line_graph(g) + @test nv(lg) == 2 # only 2 edges (self-loop counts once) + @test ne(lg) == 1 # only connection between edge 1-2 and self-loop 2-2 + end + end + @testset "Directed Line Graph" begin @testset "Directed Cycle Graphs" begin for n in 3:9 @@ -360,6 +402,7 @@ @test nv(lg) == n @test ne(lg) == n @test is_directed(lg) + @test is_connected(lg) @test all(outdegree(lg, v) == 1 for v in vertices(lg)) @test all(indegree(lg, v) == 1 for v in vertices(lg)) end @@ -373,91 +416,32 @@ @test ne(lg) == n - 2 @test is_directed(lg) @test is_connected(lg) - @test all(outdegree(lg, v) <= 1 for v in vertices(lg)) - @test all(indegree(lg, v) <= 1 for v in vertices(lg)) - if n > 2 - @test indegree(lg, 1) == 0 - @test outdegree(lg, 1) == 1 - @test indegree(lg, nv(lg)) == 1 - @test outdegree(lg, nv(lg)) == 0 - end + @test all(outdegree(lg, v) == (v < n - 1 ? 1 : 0) for v in vertices(lg)) + @test all(indegree(lg, v) == (v > 1 ? 1 : 0) for v in vertices(lg)) end end @testset "Directed Star Graphs" begin - for n in 3:9 - g = star_digraph(n) - lg = line_graph(g) - @test nv(lg) == n - 1 - @test ne(lg) == 0 - end - - for n in 3:9 - g = SimpleDiGraph(n) - for i in 2:n - add_edge!(g, 1, i) - add_edge!(g, i, 1) - end - lg = line_graph(g) - @test nv(lg) == 2*(n-1) - @test ne(lg) == (n-1) + (n-1)*(n-1) + for m in 0:4, n in 0:4 + g = SimpleDiGraph(m + n + 1) + foreach(i -> add_edge!(g, i + 1, 1), 1:m) + foreach(j -> add_edge!(g, 1, j + 1 + m), 1:n) + lg = line_graph(g) # checking if lg is the complete bipartite digraph + @test nv(lg) == m + n + @test ne(lg) == m * n + @test all(outdegree(lg, v) == 0 && indegree(lg, v) == m for v in 1:n) + @test all( + outdegree(lg, v) == n && indegree(lg, v) == 0 for v in (n + 1):(n + m) + ) end end @testset "Directed Self-loops" begin - g = SimpleDiGraph(2) - add_edge!(g, 1, 1) - add_edge!(g, 1, 2) + g = SimpleDiGraph(2, [[1, 2], Int[], Int[]], [[1], [1], Int[]]) lg = line_graph(g) @test nv(lg) == 2 - @test ne(lg) == 2 - @test has_edge(lg, 1, 1) + @test ne(lg) == 1 @test has_edge(lg, 1, 2) - @test !has_edge(lg, 2, 1) - @test !has_edge(lg, 2, 2) - end - end -end - -@testset "Undirected Line Graph" begin - @testset "Undirected Cycle Graphs" begin - for n in 3:9 - g = cycle_graph(n) - lg = line_graph(g) # checking if lg is an n-cycle - @test nv(lg) == n - @test ne(lg) == n - @test is_connected(lg) - @test all(degree(lg, v) == 2 for v in vertices(lg)) - end - end - - @testset "Undirected Path Graphs" begin - for n in 2:9 - g = path_graph(n) - lg = line_graph(g) # checking if lg is an n-1-path - @test nv(lg) == n - 1 - @test ne(lg) == n - 2 - @test is_connected(lg) - @test all(degree(lg, v) <= 2 for v in vertices(lg)) - @test any(degree(lg, v) == 1 for v in vertices(lg)) || n == 2 && ne(lg) == 0 end end - - @testset "Undirected Star Graphs" begin - for n in 3:9 - g = star_graph(n) - lg = line_graph(g) # checking if lg is a complete graph on n-1 vertices - @test nv(lg) == n - 1 - @test ne(lg) == binomial(n - 1, 2) # lg must be a complete graph - end - end - - @testset "Undirected Self-loops" begin - g = SimpleGraph(2, [[2], [1, 2], Int[]]) - lg = line_graph(g) - @test nv(lg) == 2 # only 2 edges (self-loop counts once) - @test ne(lg) == 1 # only connection between edge 1-2 and self-loop 2-2 - end - - end From e8a013dfc5ed11250b22eb432bb31e6d7bea8819 Mon Sep 17 00:00:00 2001 From: LeonLampret Date: Tue, 10 Jun 2025 22:10:17 +0200 Subject: [PATCH 09/12] renamed variables to f/badjlist and added tests for graphs over Int8,...,UInt128 --- Manifest.toml | 244 ++++++++++++++++++++++++++++++++++++++++++++++ Project.toml | 1 + src/operators.jl | 32 +++--- test/operators.jl | 24 +++-- 4 files changed, 276 insertions(+), 25 deletions(-) create mode 100644 Manifest.toml diff --git a/Manifest.toml b/Manifest.toml new file mode 100644 index 000000000..e3d9169fe --- /dev/null +++ b/Manifest.toml @@ -0,0 +1,244 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.12.0-beta4" +manifest_format = "2.0" +project_hash = "bcaf375c0caf49e051cc266bfbea08524def119d" + +[[deps.ArnoldiMethod]] +deps = ["LinearAlgebra", "Random", "StaticArrays"] +git-tree-sha1 = "d57bd3762d308bded22c3b82d033bff85f6195c6" +uuid = "ec485272-7323-5ecc-a04f-4719b315124d" +version = "0.4.0" + +[[deps.Artifacts]] +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +version = "1.11.0" + +[[deps.Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +version = "1.11.0" + +[[deps.CSTParser]] +deps = ["Tokenize"] +git-tree-sha1 = "0157e592151e39fa570645e2b2debcdfb8a0f112" +uuid = "00ebfdb7-1f24-5e51-bd34-a7502290713f" +version = "3.4.3" + +[[deps.CommonMark]] +deps = ["Crayons", "PrecompileTools"] +git-tree-sha1 = "5fdf00d1979fd4883b44b754fc3423175c9504b4" +uuid = "a80b9123-70ca-4bc0-993e-6e3bcb318db6" +version = "0.8.16" + +[[deps.Compat]] +deps = ["TOML", "UUIDs"] +git-tree-sha1 = "8ae8d32e09f0dcf42a36b90d4e17f5dd2e4c4215" +uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" +version = "4.16.0" +weakdeps = ["Dates", "LinearAlgebra"] + + [deps.Compat.extensions] + CompatLinearAlgebraExt = "LinearAlgebra" + +[[deps.CompilerSupportLibraries_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" +version = "1.3.0+1" + +[[deps.Crayons]] +git-tree-sha1 = "249fe38abf76d48563e2f4556bebd215aa317e15" +uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f" +version = "4.1.1" + +[[deps.DataStructures]] +deps = ["Compat", "InteractiveUtils", "OrderedCollections"] +git-tree-sha1 = "4e1fe97fdaed23e9dc21d4d664bea76b65fc50a0" +uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +version = "0.18.22" + +[[deps.Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" +version = "1.11.0" + +[[deps.Distributed]] +deps = ["Random", "Serialization", "Sockets"] +uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" +version = "1.11.0" + +[[deps.Glob]] +git-tree-sha1 = "97285bbd5230dd766e9ef6749b80fc617126d496" +uuid = "c27321d9-0574-5035-807b-f59d2c89b15c" +version = "1.3.1" + +[[deps.Graphs]] +deps = ["ArnoldiMethod", "DataStructures", "Distributed", "Inflate", "LinearAlgebra", "Random", "SharedArrays", "SimpleTraits", "SparseArrays", "Statistics"] +path = "." +uuid = "86223c79-3864-5bf0-83f7-82e725a168b6" +version = "1.13.0" + +[[deps.Inflate]] +git-tree-sha1 = "d1b1b796e47d94588b3757fe84fbf65a5ec4a80d" +uuid = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9" +version = "0.1.5" + +[[deps.InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +version = "1.11.0" + +[[deps.JuliaFormatter]] +deps = ["CSTParser", "CommonMark", "DataStructures", "Glob", "PrecompileTools", "TOML", "Tokenize"] +git-tree-sha1 = "59cf7ad64f1b0708a4fa4369879d33bad3239b56" +uuid = "98e50ef6-434e-11e9-1051-2b60c6c9e899" +version = "1.0.62" + +[[deps.JuliaSyntaxHighlighting]] +deps = ["StyledStrings"] +uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011" +version = "1.12.0" + +[[deps.Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" +version = "1.11.0" + +[[deps.LinearAlgebra]] +deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] +uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +version = "1.12.0" + +[[deps.MacroTools]] +git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522" +uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +version = "0.5.16" + +[[deps.Markdown]] +deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" +version = "1.11.0" + +[[deps.Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" +version = "1.11.0" + +[[deps.OpenBLAS_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] +uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" +version = "0.3.29+0" + +[[deps.OrderedCollections]] +git-tree-sha1 = "05868e21324cede2207c6f0f466b4bfef6d5e7ee" +uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +version = "1.8.1" + +[[deps.PrecompileTools]] +deps = ["Preferences"] +git-tree-sha1 = "516f18f048a195409d6e072acf879a9f017d3900" +uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +version = "1.3.2" + +[[deps.Preferences]] +deps = ["TOML"] +git-tree-sha1 = "9306f6085165d270f7e3db02af26a400d580f5c6" +uuid = "21216c6a-2e73-6563-6e65-726566657250" +version = "1.4.3" + +[[deps.Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" +version = "1.11.0" + +[[deps.Random]] +deps = ["SHA"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +version = "1.11.0" + +[[deps.SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" +version = "0.7.0" + +[[deps.Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +version = "1.11.0" + +[[deps.SharedArrays]] +deps = ["Distributed", "Mmap", "Random", "Serialization"] +uuid = "1a1011a3-84de-559e-8e89-a11a2f7dc383" +version = "1.11.0" + +[[deps.SimpleTraits]] +deps = ["InteractiveUtils", "MacroTools"] +git-tree-sha1 = "5d7e3f4e11935503d3ecaf7186eac40602e7d231" +uuid = "699a6c99-e7fa-54fc-8d76-47d257e15c1d" +version = "0.9.4" + +[[deps.Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" +version = "1.11.0" + +[[deps.SparseArrays]] +deps = ["Libdl", "LinearAlgebra", "Random", "Serialization", "SuiteSparse_jll"] +uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +version = "1.12.0" + +[[deps.StaticArrays]] +deps = ["LinearAlgebra", "PrecompileTools", "Random", "StaticArraysCore"] +git-tree-sha1 = "0feb6b9031bd5c51f9072393eb5ab3efd31bf9e4" +uuid = "90137ffa-7385-5640-81b9-e52037218182" +version = "1.9.13" + + [deps.StaticArrays.extensions] + StaticArraysChainRulesCoreExt = "ChainRulesCore" + StaticArraysStatisticsExt = "Statistics" + + [deps.StaticArrays.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" + Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" + +[[deps.StaticArraysCore]] +git-tree-sha1 = "192954ef1208c7019899fbf8049e717f92959682" +uuid = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" +version = "1.4.3" + +[[deps.Statistics]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "ae3bb1eb3bba077cd276bc5cfc337cc65c3075c0" +uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +version = "1.11.1" +weakdeps = ["SparseArrays"] + + [deps.Statistics.extensions] + SparseArraysExt = ["SparseArrays"] + +[[deps.StyledStrings]] +uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" +version = "1.11.0" + +[[deps.SuiteSparse_jll]] +deps = ["Artifacts", "Libdl", "libblastrampoline_jll"] +uuid = "bea87d4a-7f5b-5778-9afe-8cc45184846c" +version = "7.8.3+2" + +[[deps.TOML]] +deps = ["Dates"] +uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" +version = "1.0.3" + +[[deps.Tokenize]] +git-tree-sha1 = "468b4685af4abe0e9fd4d7bf495a6554a6276e75" +uuid = "0796e94c-ce3b-5d07-9a54-7f471281c624" +version = "0.5.29" + +[[deps.UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +version = "1.11.0" + +[[deps.Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" +version = "1.11.0" + +[[deps.libblastrampoline_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" +version = "5.12.0+0" diff --git a/Project.toml b/Project.toml index f869ac359..1727bd673 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,7 @@ ArnoldiMethod = "ec485272-7323-5ecc-a04f-4719b315124d" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" Inflate = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9" +JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SharedArrays = "1a1011a3-84de-559e-8e89-a11a2f7dc383" diff --git a/src/operators.jl b/src/operators.jl index 793db3f86..b07dda91a 100644 --- a/src/operators.jl +++ b/src/operators.jl @@ -881,7 +881,7 @@ function merge_vertices!(g::Graph{T}, vs::Vector{U} where {U<:Integer}) where {T end """ - line_graph(g::SimpleGraph) + line_graph(g::SimpleGraph) ::SimpleGraph Given a graph `g`, return the graph `lg`, whose vertices are integers that enumerate the edges in `g`, and two vertices in `lg` form an edge iff the corresponding edges in `g` share a common endpoint. In other words, edges in `lg` are length-2 paths in `g`. @@ -897,7 +897,7 @@ julia> lg = line_graph(g) {4, 3} undirected simple Int64 graph ``` """ -function line_graph(g::SimpleGraph)::SimpleGraph +function line_graph(g::SimpleGraph) vertex_to_edges = [Int[] for _ in 1:nv(g)] for (i, e) in enumerate(edges(g)) s, d = src(e), dst(e) @@ -906,24 +906,24 @@ function line_graph(g::SimpleGraph)::SimpleGraph push!(vertex_to_edges[d], i) end - edge_to_neighbors = [Int[] for _ in 1:ne(g)] + fadjlist = [Int[] for _ in 1:ne(g)] # edge to neighbors adjacency in lg m = 0 # number of edges in the line-graph for es in vertex_to_edges n = length(es) for i in 1:(n - 1), j in (i + 1):n # iterate through pairs of edges with same endpoint ei, ej = es[i], es[j] m += 1 - push!(edge_to_neighbors[ei], ej) - push!(edge_to_neighbors[ej], ei) + push!(fadjlist[ei], ej) + push!(fadjlist[ej], ei) end end - foreach(sort!, edge_to_neighbors) - return SimpleGraph(m, edge_to_neighbors) + foreach(sort!, fadjlist) + return SimpleGraph(m, fadjlist) end """ - line_graph(g::SimpleDiGraph) + line_graph(g::SimpleDiGraph) ::SimpleDiGraph Given a digraph `g`, return the digraph `lg`, whose vertices are integers that enumerate the edges in `g`, and there is an edge in `lg` from `Edge(a,b)` to `Edge(c,d)` iff b==c. In other words, edges in `lg` are length-2 directed paths in `g`. @@ -939,7 +939,7 @@ julia> lg = line_graph(g) {5, 5} directed simple Int64 graph ``` """ -function line_graph(g::SimpleDiGraph)::SimpleDiGraph +function line_graph(g::SimpleDiGraph) vertex_to_edgesout = [Int[] for _ in 1:nv(g)] vertex_to_edgesin = [Int[] for _ in 1:nv(g)] for (i, e) in enumerate(edges(g)) @@ -948,19 +948,19 @@ function line_graph(g::SimpleDiGraph)::SimpleDiGraph push!(vertex_to_edgesin[d], i) end - edge_to_neighborsout = [Int[] for _ in 1:ne(g)] - edge_to_neighborsin = [Int[] for _ in 1:ne(g)] + fadjilist = [Int[] for _ in 1:ne(g)] # edge to neighbors forward adjacency in lg + badjilist = [Int[] for _ in 1:ne(g)] # edge to neighbors backward adjacency in lg m = 0 # number of edges in the line-graph for (e_i, e_o) in zip(vertex_to_edgesin, vertex_to_edgesout) for ei in e_i, eo in e_o # iterate through length-2 directed paths ei == eo && continue # a self-loop in g does not induce a self-loop in lg m += 1 - push!(edge_to_neighborsout[ei], eo) - push!(edge_to_neighborsin[eo], ei) + push!(fadjilist[ei], eo) + push!(badjilist[eo], ei) end end - foreach(sort!, edge_to_neighborsout) - foreach(sort!, edge_to_neighborsin) - return SimpleDiGraph(m, edge_to_neighborsout, edge_to_neighborsin) + foreach(sort!, fadjilist) + foreach(sort!, badjilist) + return SimpleDiGraph(m, fadjilist, badjilist) end diff --git a/test/operators.jl b/test/operators.jl index d92a4bff9..2bcda7624 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -387,10 +387,13 @@ end @testset "Undirected Self-loops" begin - g = SimpleGraph(2, [[2], [1, 2], Int[]]) - lg = line_graph(g) - @test nv(lg) == 2 # only 2 edges (self-loop counts once) - @test ne(lg) == 1 # only connection between edge 1-2 and self-loop 2-2 + for T in + (Int8, Int16, Int32, Int64, Int128, UInt8, UInt16, UInt32, UInt64, UInt128) + g = SimpleGraph{T}(2, [T[2], T[1, 2], T[]]) + lg = line_graph(g) + @test nv(lg) == 2 # only 2 edges (self-loop counts once) + @test ne(lg) == 1 # only connection between edge 1-2 and self-loop 2-2 + end end end @@ -437,11 +440,14 @@ end @testset "Directed Self-loops" begin - g = SimpleDiGraph(2, [[1, 2], Int[], Int[]], [[1], [1], Int[]]) - lg = line_graph(g) - @test nv(lg) == 2 - @test ne(lg) == 1 - @test has_edge(lg, 1, 2) + for T in + (Int8, Int16, Int32, Int64, Int128, UInt8, UInt16, UInt32, UInt64, UInt128) + g = SimpleDiGraph{T}(2, [T[1, 2], T[], T[]], [T[1], T[1], T[]]) + lg = line_graph(g) + @test nv(lg) == 2 + @test ne(lg) == 1 + @test has_edge(lg, 1, 2) + end end end end From 33f8a195a394fd0493f2434c6d23e058c7d7383b Mon Sep 17 00:00:00 2001 From: lampretl Date: Thu, 12 Jun 2025 22:19:27 +0200 Subject: [PATCH 10/12] Stop tracking Manifest.toml (was committed by mistake) --- Manifest.toml | 244 -------------------------------------------------- 1 file changed, 244 deletions(-) delete mode 100644 Manifest.toml diff --git a/Manifest.toml b/Manifest.toml deleted file mode 100644 index e3d9169fe..000000000 --- a/Manifest.toml +++ /dev/null @@ -1,244 +0,0 @@ -# This file is machine-generated - editing it directly is not advised - -julia_version = "1.12.0-beta4" -manifest_format = "2.0" -project_hash = "bcaf375c0caf49e051cc266bfbea08524def119d" - -[[deps.ArnoldiMethod]] -deps = ["LinearAlgebra", "Random", "StaticArrays"] -git-tree-sha1 = "d57bd3762d308bded22c3b82d033bff85f6195c6" -uuid = "ec485272-7323-5ecc-a04f-4719b315124d" -version = "0.4.0" - -[[deps.Artifacts]] -uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" -version = "1.11.0" - -[[deps.Base64]] -uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" -version = "1.11.0" - -[[deps.CSTParser]] -deps = ["Tokenize"] -git-tree-sha1 = "0157e592151e39fa570645e2b2debcdfb8a0f112" -uuid = "00ebfdb7-1f24-5e51-bd34-a7502290713f" -version = "3.4.3" - -[[deps.CommonMark]] -deps = ["Crayons", "PrecompileTools"] -git-tree-sha1 = "5fdf00d1979fd4883b44b754fc3423175c9504b4" -uuid = "a80b9123-70ca-4bc0-993e-6e3bcb318db6" -version = "0.8.16" - -[[deps.Compat]] -deps = ["TOML", "UUIDs"] -git-tree-sha1 = "8ae8d32e09f0dcf42a36b90d4e17f5dd2e4c4215" -uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" -version = "4.16.0" -weakdeps = ["Dates", "LinearAlgebra"] - - [deps.Compat.extensions] - CompatLinearAlgebraExt = "LinearAlgebra" - -[[deps.CompilerSupportLibraries_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" -version = "1.3.0+1" - -[[deps.Crayons]] -git-tree-sha1 = "249fe38abf76d48563e2f4556bebd215aa317e15" -uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f" -version = "4.1.1" - -[[deps.DataStructures]] -deps = ["Compat", "InteractiveUtils", "OrderedCollections"] -git-tree-sha1 = "4e1fe97fdaed23e9dc21d4d664bea76b65fc50a0" -uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" -version = "0.18.22" - -[[deps.Dates]] -deps = ["Printf"] -uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" -version = "1.11.0" - -[[deps.Distributed]] -deps = ["Random", "Serialization", "Sockets"] -uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" -version = "1.11.0" - -[[deps.Glob]] -git-tree-sha1 = "97285bbd5230dd766e9ef6749b80fc617126d496" -uuid = "c27321d9-0574-5035-807b-f59d2c89b15c" -version = "1.3.1" - -[[deps.Graphs]] -deps = ["ArnoldiMethod", "DataStructures", "Distributed", "Inflate", "LinearAlgebra", "Random", "SharedArrays", "SimpleTraits", "SparseArrays", "Statistics"] -path = "." -uuid = "86223c79-3864-5bf0-83f7-82e725a168b6" -version = "1.13.0" - -[[deps.Inflate]] -git-tree-sha1 = "d1b1b796e47d94588b3757fe84fbf65a5ec4a80d" -uuid = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9" -version = "0.1.5" - -[[deps.InteractiveUtils]] -deps = ["Markdown"] -uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" -version = "1.11.0" - -[[deps.JuliaFormatter]] -deps = ["CSTParser", "CommonMark", "DataStructures", "Glob", "PrecompileTools", "TOML", "Tokenize"] -git-tree-sha1 = "59cf7ad64f1b0708a4fa4369879d33bad3239b56" -uuid = "98e50ef6-434e-11e9-1051-2b60c6c9e899" -version = "1.0.62" - -[[deps.JuliaSyntaxHighlighting]] -deps = ["StyledStrings"] -uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011" -version = "1.12.0" - -[[deps.Libdl]] -uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" -version = "1.11.0" - -[[deps.LinearAlgebra]] -deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] -uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -version = "1.12.0" - -[[deps.MacroTools]] -git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522" -uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" -version = "0.5.16" - -[[deps.Markdown]] -deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] -uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" -version = "1.11.0" - -[[deps.Mmap]] -uuid = "a63ad114-7e13-5084-954f-fe012c677804" -version = "1.11.0" - -[[deps.OpenBLAS_jll]] -deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] -uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" -version = "0.3.29+0" - -[[deps.OrderedCollections]] -git-tree-sha1 = "05868e21324cede2207c6f0f466b4bfef6d5e7ee" -uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" -version = "1.8.1" - -[[deps.PrecompileTools]] -deps = ["Preferences"] -git-tree-sha1 = "516f18f048a195409d6e072acf879a9f017d3900" -uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" -version = "1.3.2" - -[[deps.Preferences]] -deps = ["TOML"] -git-tree-sha1 = "9306f6085165d270f7e3db02af26a400d580f5c6" -uuid = "21216c6a-2e73-6563-6e65-726566657250" -version = "1.4.3" - -[[deps.Printf]] -deps = ["Unicode"] -uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" -version = "1.11.0" - -[[deps.Random]] -deps = ["SHA"] -uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -version = "1.11.0" - -[[deps.SHA]] -uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" -version = "0.7.0" - -[[deps.Serialization]] -uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" -version = "1.11.0" - -[[deps.SharedArrays]] -deps = ["Distributed", "Mmap", "Random", "Serialization"] -uuid = "1a1011a3-84de-559e-8e89-a11a2f7dc383" -version = "1.11.0" - -[[deps.SimpleTraits]] -deps = ["InteractiveUtils", "MacroTools"] -git-tree-sha1 = "5d7e3f4e11935503d3ecaf7186eac40602e7d231" -uuid = "699a6c99-e7fa-54fc-8d76-47d257e15c1d" -version = "0.9.4" - -[[deps.Sockets]] -uuid = "6462fe0b-24de-5631-8697-dd941f90decc" -version = "1.11.0" - -[[deps.SparseArrays]] -deps = ["Libdl", "LinearAlgebra", "Random", "Serialization", "SuiteSparse_jll"] -uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" -version = "1.12.0" - -[[deps.StaticArrays]] -deps = ["LinearAlgebra", "PrecompileTools", "Random", "StaticArraysCore"] -git-tree-sha1 = "0feb6b9031bd5c51f9072393eb5ab3efd31bf9e4" -uuid = "90137ffa-7385-5640-81b9-e52037218182" -version = "1.9.13" - - [deps.StaticArrays.extensions] - StaticArraysChainRulesCoreExt = "ChainRulesCore" - StaticArraysStatisticsExt = "Statistics" - - [deps.StaticArrays.weakdeps] - ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" - Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" - -[[deps.StaticArraysCore]] -git-tree-sha1 = "192954ef1208c7019899fbf8049e717f92959682" -uuid = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" -version = "1.4.3" - -[[deps.Statistics]] -deps = ["LinearAlgebra"] -git-tree-sha1 = "ae3bb1eb3bba077cd276bc5cfc337cc65c3075c0" -uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" -version = "1.11.1" -weakdeps = ["SparseArrays"] - - [deps.Statistics.extensions] - SparseArraysExt = ["SparseArrays"] - -[[deps.StyledStrings]] -uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" -version = "1.11.0" - -[[deps.SuiteSparse_jll]] -deps = ["Artifacts", "Libdl", "libblastrampoline_jll"] -uuid = "bea87d4a-7f5b-5778-9afe-8cc45184846c" -version = "7.8.3+2" - -[[deps.TOML]] -deps = ["Dates"] -uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" -version = "1.0.3" - -[[deps.Tokenize]] -git-tree-sha1 = "468b4685af4abe0e9fd4d7bf495a6554a6276e75" -uuid = "0796e94c-ce3b-5d07-9a54-7f471281c624" -version = "0.5.29" - -[[deps.UUIDs]] -deps = ["Random", "SHA"] -uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" -version = "1.11.0" - -[[deps.Unicode]] -uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" -version = "1.11.0" - -[[deps.libblastrampoline_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" -version = "5.12.0+0" From d2fdc7353f7e8dc773461cde29fe727dcfce9a74 Mon Sep 17 00:00:00 2001 From: lampretl Date: Fri, 13 Jun 2025 22:07:12 +0200 Subject: [PATCH 11/12] removed JuliaFormatter from dependencies --- Project.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Project.toml b/Project.toml index 1727bd673..f869ac359 100644 --- a/Project.toml +++ b/Project.toml @@ -7,7 +7,6 @@ ArnoldiMethod = "ec485272-7323-5ecc-a04f-4719b315124d" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" Inflate = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9" -JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SharedArrays = "1a1011a3-84de-559e-8e89-a11a2f7dc383" From 2d8c8b2308f39e2152e4127f5a5f6538108e4349 Mon Sep 17 00:00:00 2001 From: lampretl Date: Mon, 6 Apr 2026 00:22:28 +0200 Subject: [PATCH 12/12] improved line_graph and inserted changes into the latest main commit --- .git-blame-ignore-revs | 7 + .gitignore | 5 +- .pre-commit-config.yaml | 5 - CHANGELOG.md | 84 ++ Project.toml | 43 +- README.md | 23 +- docs/Project.toml | 2 + docs/make.jl | 16 +- docs/src/algorithms/community.md | 1 + docs/src/algorithms/spanningtrees.md | 2 + docs/src/core_functions/wrappedgraphs.md | 21 + docs/src/ecosystem/graphalgorithms.md | 57 + docs/src/ecosystem/interface.md | 2 +- docs/src/first_steps/paths_traversal.md | 1 - ext/GraphsSharedArraysExt.jl | 170 +++ .../ShortestPaths/ShortestPaths.jl | 2 +- src/Experimental/ShortestPaths/astar.jl | 4 +- src/Experimental/ShortestPaths/dijkstra.jl | 2 +- src/Experimental/Traversals/bfs.jl | 1 - src/Experimental/vf2.jl | 10 +- src/Graphs.jl | 39 +- src/Parallel/Parallel.jl | 2 - src/Parallel/centrality/betweenness.jl | 39 +- src/Parallel/centrality/closeness.jl | 36 +- src/Parallel/centrality/radiality.jl | 22 +- src/Parallel/centrality/stress.jl | 23 +- src/Parallel/distance.jl | 41 +- src/Parallel/shortestpaths/dijkstra.jl | 38 +- src/Parallel/traversals/greedy_color.jl | 34 +- src/Parallel/utils.jl | 12 +- src/SimpleGraphs/SimpleGraphs.jl | 6 +- src/SimpleGraphs/generators/randgraphs.jl | 2 +- src/SimpleGraphs/generators/smallgraphs.jl | 1052 ++++++++--------- src/SimpleGraphs/generators/staticgraphs.jl | 62 +- src/SimpleGraphs/simpledigraph.jl | 9 + src/SimpleGraphs/simpleedgeiter.jl | 4 +- src/SimpleGraphs/simplegraph.jl | 10 +- src/biconnectivity/articulation.jl | 177 ++- src/centrality/betweenness.jl | 17 +- src/community/clique_percolation.jl | 2 +- src/community/cliques.jl | 122 ++ src/community/louvain.jl | 271 +++++ src/community/modularity.jl | 18 +- src/connectivity.jl | 100 +- src/core.jl | 18 +- src/cycles/basis.jl | 2 +- src/distance.jl | 121 +- src/dominatingset/degree_dom_set.jl | 4 +- src/editdist.jl | 14 +- src/frozenvector.jl | 25 + src/graphcut/karger_min_cut.jl | 2 +- src/independentset/degree_ind_set.jl | 5 +- src/independentset/maximal_ind_set.jl | 3 + src/interface.jl | 14 +- src/linalg/nonbacktracking.jl | 9 +- src/linalg/spectral.jl | 4 +- src/operators.jl | 358 +++++- src/persistence/lg.jl | 8 +- src/planarity.jl | 454 +++++++ src/shortestpaths/astar.jl | 4 +- src/shortestpaths/dijkstra.jl | 2 +- src/shortestpaths/yen.jl | 8 +- src/spanningtrees/boruvka.jl | 2 +- src/spanningtrees/kruskal.jl | 24 +- .../planar_maximally_filtered_graph.jl | 60 + src/spanningtrees/prim.jl | 2 +- src/traversals/maxadjvisit.jl | 6 +- src/vertexcover/degree_vertex_cover.jl | 4 +- src/wrappedGraphs/graphviews.jl | 136 +++ test/Project.toml | 23 + test/biconnectivity/articulation.jl | 16 +- test/centrality/eigenvector.jl | 3 +- test/community/cliques.jl | 7 +- test/community/independent_sets.jl | 34 + test/community/louvain.jl | 157 +++ test/community/modularity.jl | 41 +- test/distance.jl | 76 ++ test/operators.jl | 84 +- test/parallel/distance.jl | 12 +- test/parallel/runtests.jl | 1 + test/parallel/shortestpaths/dijkstra.jl | 22 +- test/parallel/traversals/greedy_color.jl | 18 +- test/planarity.jl | 121 ++ test/runtests.jl | 36 +- test/simplegraphs/generators/staticgraphs.jl | 17 + test/simplegraphs/runtests.jl | 4 +- test/simplegraphs/simplegraphs.jl | 13 +- test/spanningtrees/boruvka.jl | 24 +- test/spanningtrees/kruskal.jl | 18 + .../planar_maximally_filtered_graph.jl | 49 + test/wrappedGraphs/graphviews.jl | 105 ++ 91 files changed, 3748 insertions(+), 1018 deletions(-) create mode 100644 .git-blame-ignore-revs delete mode 100644 .pre-commit-config.yaml create mode 100644 CHANGELOG.md create mode 100644 docs/src/core_functions/wrappedgraphs.md create mode 100644 docs/src/ecosystem/graphalgorithms.md create mode 100644 ext/GraphsSharedArraysExt.jl create mode 100644 src/community/louvain.jl create mode 100644 src/frozenvector.jl create mode 100644 src/planarity.jl create mode 100644 src/spanningtrees/planar_maximally_filtered_graph.jl create mode 100644 src/wrappedGraphs/graphviews.jl create mode 100644 test/Project.toml create mode 100644 test/community/independent_sets.jl create mode 100644 test/community/louvain.jl create mode 100644 test/planarity.jl create mode 100644 test/spanningtrees/planar_maximally_filtered_graph.jl create mode 100644 test/wrappedGraphs/graphviews.jl diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..f372b77a9 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,7 @@ +# .git-blame-ignore-revs +# Initial converion to uniform formatting +ea6bcfeb71de9d1229bbd411ef9b10cac44fa795 +# Formatter v1 improvements +5878e7be4d68b2a1c179d1367aea670db115ebb5 +# updating to Formatter v2 +f4b70cd8bbbcd43bf127bbe2acf3f7a11aef7942 diff --git a/.gitignore b/.gitignore index 5628095a6..4097042e9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,12 +6,9 @@ docs/build/ docs/site/ benchmark/.results/* benchmark/.tune.jld -benchmark/Manifest.toml .benchmarkci *.cov -/Manifest.toml -/docs/Manifest.toml +Manifest.toml /docs/src/index.md /docs/src/contributing.md /docs/src/license.md -.aider* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 592195822..000000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,5 +0,0 @@ -repos: -- repo: "https://github.com/domluna/JuliaFormatter.jl" - rev: "v1.0.62" - hooks: - - id: "julia-formatter" \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..30638257c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,84 @@ +# News + +We follow SemVer as most of the Julia ecosystem. Below you might see the "breaking" label even for minor version bumps -- we use it a bit more loosely to denote things that are not breaking by SemVer's definition but might cause breakage to people using internal or experimental APIs or undocumented implementation details. + +## unreleased +- `is_articulation(g, v)` for checking whether a single vertex is an articulation point + + +## v1.14.0 - 2026-02-26 +- **(breaking)** `neighbors`, `inneighbors`, and `outneighbors` now return an immutable `FrozenVector` instead of `Vector` +- Louvain community detection algorithm +- Graph views: `ReverseView` and `UndirectedView` for directed graphs +- New graph products: `strong_product`, `disjunctive_product`, `lexicographic_product`, `homomorphic_product` +- `maximum_clique`, `clique_number`, `maximal_independent_sets`, `maximum_independent_set`, `independence_number` +- `regular_tree` generator +- `kruskal_mst` now accepts weight vectors +- `is_planar` planarity test and `planar_maximally_filtered_graph` (PMFG) algorithm +- `count_connected_components` for efficiently counting connected components without materializing them +- `connected_components!` is now exported and accepts an optional `search_queue` argument to reduce allocations +- `is_connected` optimized to avoid allocating component vectors + +## v1.13.0 - 2025-06-05 +- **(breaking)** Julia v1.10 (LTS) minimum version requirement +- Non-allocating `enumerate_paths!` + +## v1.12.0 - 2024-09-29 +- New options for `BFSIterator` + +## v1.11.0 - 2024-05-05 +- DFS and BFS iterators +- Dorogovtsev-Mendes graph generator optimization + +## v1.10.0 - 2024-04-05 +- Longest path algorithm for DAGs +- All simple paths algorithm + +## v1.9.0 - 2023-09-28 +- Rewrite of `edit_distance` with edge costs +- Eulerian cycles/trails for undirected graphs +- `mincut` implementation +- `strongly_connected_components_tarjan` + +## v1.8.0 - 2023-02-10 +- `newman_watts_strogatz` graph generator +- Prufer coding for trees +- `isdigraphical` + +## v1.7.0 - 2022-06-19 +- Hierarchical documentation structure + +## v1.6.0 - 2022-02-09 +- **(breaking)** Requires Julia >= v1.6 +- **(breaking)** `Base.zero` no longer mandatory for `AbstractGraph` +- Simplified `AbstractGraph` interface + +## v1.5.0 - 2022-01-09 +- **(breaking)** `merge_vertices` now only works on subtypes of `AbstractSimpleGraph` +- `rich_club` function +- `induced_subgraph` with boolean indexing +- Optional start vertex for `maximum_adjacency_search` + +## v1.4.0 - 2021-10-17 +- Initial release as Graphs.jl (successor to LightGraphs.jl) + +The _Graphs.jl_ project is a reboot of the _LightGraphs.jl_ package (archived in October 2021), which remains available on GitHub at [sbromberger/LightGraphs.jl](https://github.com/sbromberger/LightGraphs.jl). If you don't need any new features developed since the fork, you can continue to use older versions of _LightGraphs.jl_ indefinitely. New versions will be released here using the name _Graphs.jl_ instead of _LightGraphs.jl_. There was an older package also called _Graphs.jl_. The source history and versions are still available in this repository, but the current code base is unrelated to the old _Graphs.jl_ code and is derived purely from _LightGraphs.jl_. To access the history of the old _Graphs.jl_ code, you can start from [commit 9a25019](https://github.com/JuliaGraphs/Graphs.jl/commit/9a2501948053f60c630caf9d4fb257e689629041). + +### Transition from LightGraphs to Graphs + +_LightGraphs.jl_ and _Graphs.jl_ are functionally identical, still there are some steps involved making the change: + +- Change `LightGraphs = "093fc24a-ae57-5d10-9952-331d41423f4d"` to `Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"` in your Project.toml. +- Update your `using` and `import` statements. +- Update your type constraints and other references to `LightGraphs` to `Graphs`. +- Increment your version number. Following semantic versioning, we suggest a patch release when no graphs or other `Graphs.jl`-objects can be passed through the API of your package by those depending on it, otherwise consider it a breaking release. "Passed through" entails created outside and consumed inside your package and vice versa. +- Tag a release. + +### About versions + +- The master branch of _Graphs.jl_ is generally designed to work with versions of Julia starting from the [LTS release](https://julialang.org/downloads/#long_term_support_release) all the way to the [current stable release](https://julialang.org/downloads/#current_stable_release), except during Julia version increments as we transition to the new version. +- Later versions: Some functionality might not work with prerelease / unstable / nightly versions of Julia. If you run into a problem, please file an issue. +- The project was previously developed under the name _LightGraphs.jl_ and older versions of _LightGraphs.jl_ (≤ v1.3.5) must still be used with that name. +- There was also an older package also called _Graphs.jl_ (git tags `v0.2.5` through `v0.10.3`), but the current code base here is a fork of _LightGraphs.jl_ v1.3.5. +- All older _LightGraphs.jl_ versions are tagged using the naming scheme `lg-vX.Y.Z` rather than plain `vX.Y.Z`, which is used for old _Graphs.jl_ versions (≤ v0.10) and newer versions derived from _LightGraphs.jl_ but released with the _Graphs.jl_ name (≥ v1.4). +- If you are using a version of Julia prior to 1.x, then you should use _LightGraphs.jl_ at `lg-v.12.*` or _Graphs.jl_ at `v0.10.3` diff --git a/Project.toml b/Project.toml index f869ac359..1ff85172b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,46 +1,33 @@ name = "Graphs" uuid = "86223c79-3864-5bf0-83f7-82e725a168b6" -version = "1.13.0" +version = "1.14.0" [deps] ArnoldiMethod = "ec485272-7323-5ecc-a04f-4719b315124d" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" -Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" Inflate = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -SharedArrays = "1a1011a3-84de-559e-8e89-a11a2f7dc383" SimpleTraits = "699a6c99-e7fa-54fc-8d76-47d257e15c1d" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +[weakdeps] +Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" +SharedArrays = "1a1011a3-84de-559e-8e89-a11a2f7dc383" + +[extensions] +GraphsSharedArraysExt = "SharedArrays" + [compat] -Aqua = "0.6" ArnoldiMethod = "0.4" -DataStructures = "0.17, 0.18" -Documenter = "0.27" +DataStructures = "0.19" +Distributed = "1" Inflate = "0.1.3" -JuliaFormatter = "1" -SimpleTraits = "0.9" -StableRNGs = "1" +LinearAlgebra = "1" +Random = "1" +SharedArrays = "1" +SimpleTraits = "0.9.1" +SparseArrays = "1" Statistics = "1" julia = "1.10" - -[extras] -Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" -Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" -DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" -Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" -JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" -LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" -StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" -Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" - -[targets] -test = ["Aqua", "Base64", "DelimitedFiles", "Documenter", "JET", "JuliaFormatter", "LinearAlgebra", "Pkg", "Random", "SparseArrays", "StableRNGs", "Statistics", "Test", "Unitful"] diff --git a/README.md b/README.md index 2cffdcde0..c08e24e66 100644 --- a/README.md +++ b/README.md @@ -68,25 +68,6 @@ It is an explicit design decision that any data not required for graph manipulat Additional functionality like advanced IO and file formats, weighted graphs, property graphs, and optimization-related functions can be found in the packages of the [JuliaGraphs organization](https://juliagraphs.org/). -## Project status +## Project history -The _Graphs.jl_ project is a reboot of the _LightGraphs.jl_ package (archived in October 2021), which remains available on GitHub at [sbromberger/LightGraphs.jl](https://github.com/sbromberger/LightGraphs.jl). If you don't need any new features developed since the fork, you can continue to use older versions of _LightGraphs.jl_ indefinitely. New versions will be released here using the name _Graphs.jl_ instead of _LightGraphs.jl_. There was an older package also called _Graphs.jl_. The source history and versions are still available in this repository, but the current code base is unrelated to the old _Graphs.jl_ code and is derived purely from _LightGraphs.jl_. To access the history of the old _Graphs.jl_ code, you can start from [commit 9a25019](https://github.com/JuliaGraphs/Graphs.jl/commit/9a2501948053f60c630caf9d4fb257e689629041). - -### Transition from LightGraphs to Graphs - -_LightGraphs.jl_ and _Graphs.jl_ are functionally identical, still there are some steps involved making the change: - -- Change `LightGraphs = "093fc24a-ae57-5d10-9952-331d41423f4d"` to `Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"` in your Project.toml. -- Update your `using` and `import` statements. -- Update your type constraints and other references to `LightGraphs` to `Graphs`. -- Increment your version number. Following semantic versioning, we suggest a patch release when no graphs or other `Graphs.jl`-objects can be passed through the API of your package by those depending on it, otherwise consider it a breaking release. "Passed through" entails created outside and consumed inside your package and vice versa. -- Tag a release. - -### About versions - -- The master branch of _Graphs.jl_ is generally designed to work with versions of Julia starting from the [LTS release](https://julialang.org/downloads/#long_term_support_release) all the way to the [current stable release](https://julialang.org/downloads/#current_stable_release), except during Julia version increments as we transition to the new version. -- Later versions: Some functionality might not work with prerelease / unstable / nightly versions of Julia. If you run into a problem, please file an issue. -- The project was previously developed under the name _LightGraphs.jl_ and older versions of _LightGraphs.jl_ (≤ v1.3.5) must still be used with that name. -- There was also an older package also called _Graphs.jl_ (git tags `v0.2.5` through `v0.10.3`), but the current code base here is a fork of _LightGraphs.jl_ v1.3.5. -- All older _LightGraphs.jl_ versions are tagged using the naming scheme `lg-vX.Y.Z` rather than plain `vX.Y.Z`, which is used for old _Graphs.jl_ versions (≤ v0.10) and newer versions derived from _LightGraphs.jl_ but released with the _Graphs.jl_ name (≥ v1.4). -- If you are using a version of Julia prior to 1.x, then you should use _LightGraphs.jl_ at `lg-v.12.*` or _Graphs.jl_ at `v0.10.3` +_Graphs.jl_ is the successor to _LightGraphs.jl_ (archived October 2021); see the [CHANGELOG](CHANGELOG.md) for the full transition history. diff --git a/docs/Project.toml b/docs/Project.toml index dea978a5e..b54c85a8e 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,6 +1,8 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +IGraphs = "647e90d3-2106-487c-adb4-c91fc07b96ea" +NautyGraphs = "7509a0a4-015a-4167-b44b-0799a1a2605e" [compat] Documenter = "1" diff --git a/docs/make.jl b/docs/make.jl index 8a6b91839..30797f0f3 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,5 +1,7 @@ using Documenter using Graphs +using IGraphs: IGraphs +using NautyGraphs: NautyGraphs # same for contributing and license cp( @@ -17,6 +19,11 @@ cp( normpath(@__FILE__, "../src/index.md"); force=true, ) +cp( + normpath(@__FILE__, "../../CHANGELOG.md"), + normpath(@__FILE__, "../src/CHANGELOG.md"); + force=true, +) function get_title(markdown_file_path::AbstractString) first_line = open(markdown_file_path) do io @@ -35,7 +42,11 @@ pages_files = [ "first_steps/plotting.md", "first_steps/persistence.md", ], - "Ecosystem" => ["ecosystem/graphtypes.md", "ecosystem/interface.md"], + "Ecosystem" => [ + "ecosystem/graphtypes.md", + "ecosystem/graphalgorithms.md", + "ecosystem/interface.md", + ], "Core API" => [ "core_functions/core.md", "core_functions/interface.md", @@ -44,6 +55,7 @@ pages_files = [ "core_functions/persistence.md", "core_functions/simplegraphs_generators.md", "core_functions/simplegraphs.md", + "core_functions/wrappedgraphs.md", ], "Algorithms API" => [ "algorithms/biconnectivity.md", @@ -94,6 +106,7 @@ makedocs(; canonical="https://gdalle.github.io/Graphs.jl", ), sitename="Graphs.jl", + checkdocs=:public, doctest=false, expandfirst=[], pages=[ @@ -109,3 +122,4 @@ deploydocs(; repo="github.com/JuliaGraphs/Graphs.jl.git", target="build") rm(normpath(@__FILE__, "../src/contributing.md")) rm(normpath(@__FILE__, "../src/license.md")) rm(normpath(@__FILE__, "../src/index.md")) +rm(normpath(@__FILE__, "../src/CHANGELOG.md")) diff --git a/docs/src/algorithms/community.md b/docs/src/algorithms/community.md index c8c85b737..e70de215e 100644 --- a/docs/src/algorithms/community.md +++ b/docs/src/algorithms/community.md @@ -19,6 +19,7 @@ Pages = [ "community/clustering.jl", "community/core-periphery.jl", "community/label_propagation.jl", + "community/louvain.jl", "community/modularity.jl", "community/rich_club.jl", ] diff --git a/docs/src/algorithms/spanningtrees.md b/docs/src/algorithms/spanningtrees.md index d037abd25..4a0cb54a4 100644 --- a/docs/src/algorithms/spanningtrees.md +++ b/docs/src/algorithms/spanningtrees.md @@ -16,6 +16,8 @@ Pages = [ "spanningtrees/boruvka.jl", "spanningtrees/kruskal.jl", "spanningtrees/prim.jl", + "planarity.jl", + "spanningtrees/planar_maximally_filtered_graph.jl", ] ``` diff --git a/docs/src/core_functions/wrappedgraphs.md b/docs/src/core_functions/wrappedgraphs.md new file mode 100644 index 000000000..9dcd314e1 --- /dev/null +++ b/docs/src/core_functions/wrappedgraphs.md @@ -0,0 +1,21 @@ +# Graph views formats + +*Graphs.jl* provides views around directed graphs. +`ReverseGraph` is a graph view that wraps a directed graph and reverse the direction of every edge. +`UndirectedGraph` is a graph view that wraps a directed graph and consider every edge as undirected. + +## Index + +```@index +Pages = ["wrappedgraphs.md"] +``` + +## Full docs + +```@autodocs +Modules = [Graphs] +Pages = [ + "wrappedGraphs/graphviews.jl", +] + +``` \ No newline at end of file diff --git a/docs/src/ecosystem/graphalgorithms.md b/docs/src/ecosystem/graphalgorithms.md new file mode 100644 index 000000000..044febf8d --- /dev/null +++ b/docs/src/ecosystem/graphalgorithms.md @@ -0,0 +1,57 @@ +# Graph algorithms + +## Defined by Graphs.jl + +_Graphs.jl_ provides a number of graph algorithms, including [Cuts](@ref), [Cycles](@ref), and [Trees](@ref), among many others. The algorithms work on any graph type that conforms to the _Graphs.jl_ API. + +## External algorithm packages + +Several other packages implement additional graph algorithms: + +- [GraphsColoring.jl](https://github.com/JuliaGraphs/GraphsColoring.jl) provides algorithms for graph coloring, _i.e._, assigning colors to vertices such that no two neighboring vertices have the same color. +- [GraphsFlows.jl](https://github.com/JuliaGraphs/GraphsFlows.jl) provides algorithms for graph flows. +- [GraphsMatching.jl](https://github.com/JuliaGraphs/GraphsMatching.jl) provides algorithms for matchings on weighted graphs. +- [GraphsOptim.jl](https://github.com/JuliaGraphs/GraphsOptim.jl) provides algorithms for graph optimization that rely on mathematical programming. + +## Interfaces to other graph libraries + +Several packages make established graph libraries written in other languages accessible from within Julia and the _Graphs.jl_ ecosystem: + +- [IGraphs.jl](https://github.com/JuliaGraphs/IGraphs.jl) is a thin Julia wrapper around the C graphs library [igraph](https://igraph.org). +- [NautyGraphs.jl](https://github.com/JuliaGraphs/NautyGraphs.jl) provides graph structures compatible with the graph isomorphism library [_nauty_](https://pallini.di.uniroma1.it), allowing for efficient isomorphism checking and canonization, as well as computing the properties of graph automorphism groups. + +## Dispatching to algorithm implementations in external packages + +Apart from providing additional graph types and algorithms, many packages extend existing functions in _Graphs.jl_ with new backends. This can make it easier to use the algorithms from within _Graphs.jl_. + +For example, _NautyGraphs.jl_ provides a new backend for graph isomorphism calculations: + +```jldoctest +julia> using Graphs, NautyGraphs + +julia> g = star_graph(5) +{5, 4} undirected simple Int64 graph + +julia> Graphs.Experimental.has_isomorph(g, g, NautyAlg()) +true +``` + +Here, dispatching via `NautyAlg()` implicitly converts `g` to a _nauty_-compatible format and uses _nauty_ for the isomorphism computation. + +### Functions extended by IGraphs.jl + +A list of functions extended by _IGraphs.jl_ can be obtained with + +```@example +import IGraphs +IGraphs.igraphalg_methods() +``` + +### Functions extended by NautyGraphs.jl + +A list of functions extended by _NautyGraphs.jl_ can be obtained with + +```@example +import NautyGraphs +NautyGraphs.nautyalg_methods() +``` diff --git a/docs/src/ecosystem/interface.md b/docs/src/ecosystem/interface.md index 9df171dca..01ea41b21 100644 --- a/docs/src/ecosystem/interface.md +++ b/docs/src/ecosystem/interface.md @@ -5,7 +5,7 @@ This section is designed to guide developers who wish to write their own graph s All Graphs.jl functions rely on a standard API to function. As long as your graph structure is a subtype of [`AbstractGraph`](@ref) and implements the following API functions with the given return values, all functions within the Graphs.jl package should just work: - [`edges`](@ref) -- [`edgetype`](@ref) (example: `edgetype(g::CustomGraph) = Graphs.SimpleEdge{eltype(g)})`) +- [`edgetype`](@ref) (example: `edgetype(g::CustomGraph) = Graphs.SimpleEdge{eltype(g)}`) - [`has_edge`](@ref) - [`has_vertex`](@ref) - [`inneighbors`](@ref) diff --git a/docs/src/first_steps/paths_traversal.md b/docs/src/first_steps/paths_traversal.md index 5cdaca79a..8c10426e4 100644 --- a/docs/src/first_steps/paths_traversal.md +++ b/docs/src/first_steps/paths_traversal.md @@ -10,7 +10,6 @@ Any graph traversal will traverse an edge only if it is present in the graph. Wh 1. distance values for undefined edges will be ignored; 2. any unassigned values (in sparse distance matrices) for edges that are present in the graph will be assumed to take the default value of 1.0; -3. any zero values (in sparse/dense distance matrices) for edges that are present in the graph will instead have an implicit edge cost of 1.0. ## Graph traversal diff --git a/ext/GraphsSharedArraysExt.jl b/ext/GraphsSharedArraysExt.jl new file mode 100644 index 000000000..6526cace0 --- /dev/null +++ b/ext/GraphsSharedArraysExt.jl @@ -0,0 +1,170 @@ +module GraphsSharedArraysExt + +using Graphs +using SharedArrays: SharedArrays, SharedMatrix, SharedVector, sdata +using SharedArrays.Distributed: @distributed +using Random: shuffle + +# betweenness +function Graphs.Parallel.distr_betweenness_centrality( + g::AbstractGraph, + vs=vertices(g), + distmx::AbstractMatrix=weights(g); + normalize=true, + endpoints=false, +)::Vector{Float64} + n_v = nv(g) + k = length(vs) + isdir = is_directed(g) + + # Parallel reduction + + betweenness = @distributed (+) for s in vs + temp_betweenness = zeros(n_v) + if degree(g, s) > 0 # this might be 1? + state = Graphs.dijkstra_shortest_paths( + g, s, distmx; allpaths=true, trackvertices=true + ) + if endpoints + Graphs._accumulate_endpoints!(temp_betweenness, state, g, s) + else + Graphs._accumulate_basic!(temp_betweenness, state, g, s) + end + end + temp_betweenness + end + + Graphs._rescale!(betweenness, n_v, normalize, isdir, k) + + return betweenness +end + +# closeness +function Graphs.Parallel.distr_closeness_centrality( + g::AbstractGraph, distmx::AbstractMatrix=weights(g); normalize=true +)::Vector{Float64} + n_v = Int(nv(g)) + closeness = SharedVector{Float64}(n_v) + fill!(closeness, 0.0) + + @sync @distributed for u in vertices(g) + if degree(g, u) == 0 # no need to do Dijkstra here + closeness[u] = 0.0 + else + d = Graphs.dijkstra_shortest_paths(g, u, distmx).dists + δ = filter(x -> x != typemax(x), d) + σ = sum(δ) + l = length(δ) - 1 + if σ > 0 + closeness[u] = l / σ + if normalize + n = l * 1.0 / (n_v - 1) + closeness[u] *= n + end + else + closeness[u] = 0.0 + end + end + end + return sdata(closeness) +end + +# radiality +function Graphs.Parallel.distr_radiality_centrality(g::AbstractGraph)::Vector{Float64} + n_v = nv(g) + vs = vertices(g) + n = ne(g) + meandists = SharedVector{Float64}(Int(n_v)) + maxdists = SharedVector{Float64}(Int(n_v)) + + @sync @distributed for i in 1:n_v + d = Graphs.dijkstra_shortest_paths(g, vs[i]) + maxdists[i] = maximum(d.dists) + meandists[i] = sum(d.dists) / (n_v - 1) + nothing + end + dmtr = maximum(maxdists) + radialities = collect(meandists) + return ((dmtr + 1) .- radialities) ./ dmtr +end + +# stress +function Graphs.Parallel.distr_stress_centrality( + g::AbstractGraph, vs=vertices(g) +)::Vector{Int64} + n_v = nv(g) + k = length(vs) + isdir = is_directed(g) + + # Parallel reduction + stress = @distributed (+) for s in vs + temp_stress = zeros(Int64, n_v) + if degree(g, s) > 0 # this might be 1? + state = Graphs.dijkstra_shortest_paths(g, s; allpaths=true, trackvertices=true) + Graphs._stress_accumulate_basic!(temp_stress, state, g, s) + end + temp_stress + end + return stress +end + +# generate_reduce +function Graphs.Parallel.distr_generate_reduce( + g::AbstractGraph{T}, gen_func::Function, comp::Comp, reps::Integer +) where {T<:Integer,Comp} + # Type assert required for type stability + min_set::Vector{T} = @distributed ((x, y) -> comp(x, y) ? x : y) for _ in 1:reps + gen_func(g) + end + return min_set +end + +# eccentricity +function Graphs.Parallel.distr_eccentricity( + g::AbstractGraph, vs=vertices(g), distmx::AbstractMatrix{T}=weights(g) +) where {T<:Number} + vlen = length(vs) + eccs = SharedVector{T}(vlen) + @sync @distributed for i in 1:vlen + local d = Graphs.dijkstra_shortest_paths(g, vs[i], distmx) + eccs[i] = maximum(d.dists) + end + d = sdata(eccs) + maximum(d) == typemax(T) && @warn("Infinite path length detected") + return d +end + +# dijkstra shortest paths +function Graphs.Parallel.distr_dijkstra_shortest_paths( + g::AbstractGraph{U}, sources=vertices(g), distmx::AbstractMatrix{T}=weights(g) +) where {T<:Number} where {U} + n_v = nv(g) + r_v = length(sources) + + # TODO: remove `Int` once julialang/#23029 / #23032 are resolved + dists = SharedMatrix{T}(Int(r_v), Int(n_v)) + parents = SharedMatrix{U}(Int(r_v), Int(n_v)) + + @sync @distributed for i in 1:r_v + state = Graphs.dijkstra_shortest_paths(g, sources[i], distmx) + dists[i, :] = state.dists + parents[i, :] = state.parents + end + + result = Graphs.Parallel.MultipleDijkstraState(sdata(dists), sdata(parents)) + return result +end + +# random greedy color +function Graphs.Parallel.distr_random_greedy_color( + g::AbstractGraph{T}, reps::Integer +) where {T<:Integer} + best = @distributed (Graphs.best_color) for i in 1:reps + seq = shuffle(vertices(g)) + Graphs.perm_greedy_color(g, seq) + end + + return convert(Graphs.Coloring{T}, best) +end + +end diff --git a/src/Experimental/ShortestPaths/ShortestPaths.jl b/src/Experimental/ShortestPaths/ShortestPaths.jl index ec7d8e139..ece5232ff 100644 --- a/src/Experimental/ShortestPaths/ShortestPaths.jl +++ b/src/Experimental/ShortestPaths/ShortestPaths.jl @@ -4,7 +4,7 @@ using Graphs using Graphs.Experimental.Traversals using Graphs: AbstractGraph, AbstractEdge using Graphs.SimpleGraphs: AbstractSimpleGraph -using DataStructures: PriorityQueue, enqueue!, dequeue! +using DataStructures: PriorityQueue import Graphs.Experimental.Traversals: initfn!, previsitfn!, newvisitfn!, visitfn!, postvisitfn!, postlevelfn! diff --git a/src/Experimental/ShortestPaths/astar.jl b/src/Experimental/ShortestPaths/astar.jl index f12ad5a34..fab0a8048 100644 --- a/src/Experimental/ShortestPaths/astar.jl +++ b/src/Experimental/ShortestPaths/astar.jl @@ -56,7 +56,7 @@ function a_star_impl!( total_path = Vector{T}() @inbounds while !isempty(open_set) - current = dequeue!(open_set) + current = popfirst!(open_set).first if current == goal reconstruct_path!(total_path, came_from, current, g) @@ -104,7 +104,7 @@ function shortest_paths( checkbounds(distmx, Base.OneTo(nv(g)), Base.OneTo(nv(g))) open_set = PriorityQueue{Integer,T}() - enqueue!(open_set, s, 0) + push!(open_set, s => 0) closed_set = zeros(Bool, nv(g)) diff --git a/src/Experimental/ShortestPaths/dijkstra.jl b/src/Experimental/ShortestPaths/dijkstra.jl index 9d9baea98..5fd70f4dd 100644 --- a/src/Experimental/ShortestPaths/dijkstra.jl +++ b/src/Experimental/ShortestPaths/dijkstra.jl @@ -66,7 +66,7 @@ function shortest_paths( sizehint!(closest_vertices, nvg) while !isempty(H) - u = dequeue!(H) + u = popfirst!(H).first if alg.track_vertices push!(closest_vertices, u) diff --git a/src/Experimental/Traversals/bfs.jl b/src/Experimental/Traversals/bfs.jl index 40b663fc9..8ce058c18 100644 --- a/src/Experimental/Traversals/bfs.jl +++ b/src/Experimental/Traversals/bfs.jl @@ -1,4 +1,3 @@ -import Base.Sort, Base.Sort.Algorithm import Base: sort! struct NOOPSortAlg <: Base.Sort.Algorithm end diff --git a/src/Experimental/vf2.jl b/src/Experimental/vf2.jl index d4286fe4b..0c3b44929 100644 --- a/src/Experimental/vf2.jl +++ b/src/Experimental/vf2.jl @@ -489,7 +489,7 @@ function has_induced_subgraphisomorph( edge_relation::Union{Nothing,Function}=nothing, )::Bool result = false - callback(vmap) = (result = true; return false) + callback(vmap) = (result=true; return false) vf2( callback, g1, @@ -509,7 +509,7 @@ function has_subgraphisomorph( edge_relation::Union{Nothing,Function}=nothing, )::Bool result = false - callback(vmap) = (result = true; return false) + callback(vmap) = (result=true; return false) vf2( callback, g1, @@ -531,7 +531,7 @@ function has_isomorph( !could_have_isomorph(g1, g2) && return false result = false - callback(vmap) = (result = true; return false) + callback(vmap) = (result=true; return false) vf2( callback, g1, @@ -648,8 +648,6 @@ function all_subgraphisomorph( end #! format: off -# Turns off formatting from this point onwards - function all_isomorph( g1::AbstractGraph, g2::AbstractGraph, @@ -672,6 +670,4 @@ function all_isomorph( end return ch end - #! format: on -# Turns on formatting from this point onwards diff --git a/src/Graphs.jl b/src/Graphs.jl index 6e565438c..e0c8ac437 100644 --- a/src/Graphs.jl +++ b/src/Graphs.jl @@ -8,21 +8,17 @@ using Statistics: mean using Inflate: InflateGzipStream using DataStructures: - IntDisjointSets, + IntDisjointSet, PriorityQueue, - dequeue!, - dequeue_pair!, - enqueue!, heappop!, heappush!, in_same_set, - peek, union!, find_root!, BinaryMaxHeap, BinaryMinHeap, Stack -using LinearAlgebra: I, Symmetric, diagm, eigen, eigvals, norm, rmul!, tril, triu +using LinearAlgebra: I, Symmetric, diagind, diagm, eigen, eigvals, norm, rmul!, tril, triu import LinearAlgebra: Diagonal, issymmetric, mul! using Random: AbstractRNG, @@ -118,6 +114,11 @@ export squash, weights, + # wrapped graphs + ReverseView, + UndirectedView, + wrapped_graph, + # simplegraphs add_edge!, add_vertex!, @@ -160,12 +161,16 @@ export join, tensor_product, cartesian_product, + strong_product, + disjunctive_product, + lexicographic_product, + homomorphic_product, crosspath, induced_subgraph, egonet, + line_graph, merge_vertices!, merge_vertices, - line_graph, # bfs gdistances, @@ -208,6 +213,8 @@ export # connectivity connected_components, + connected_components!, + count_connected_components, strongly_connected_components, strongly_connected_components_kosaraju, strongly_connected_components_tarjan, @@ -320,7 +327,13 @@ export global_clustering_coefficient, triangles, label_propagation, + louvain, maximal_cliques, + maximum_clique, + clique_number, + maximal_independent_sets, + maximum_independent_set, + independence_number, clique_percolation, assortativity, rich_club, @@ -342,6 +355,7 @@ export cycle_digraph, binary_tree, double_binary_tree, + regular_tree, roach_graph, clique_graph, ladder_graph, @@ -417,6 +431,7 @@ export # biconnectivity and articulation points articulation, + is_articulation, biconnected_components, bridges, @@ -436,8 +451,11 @@ export vertex_cover, # longestpaths - dag_longest_path + dag_longest_path, + # planarity + is_planar, + planar_maximally_filtered_graph """ Graphs @@ -462,9 +480,11 @@ and tutorials are available at the Graphs include("interface.jl") include("utils.jl") +include("frozenvector.jl") include("deprecations.jl") include("core.jl") +include("wrappedGraphs/graphviews.jl") include("SimpleGraphs/SimpleGraphs.jl") using .SimpleGraphs """ @@ -533,6 +553,7 @@ include("centrality/eigenvector.jl") include("centrality/radiality.jl") include("community/modularity.jl") include("community/label_propagation.jl") +include("community/louvain.jl") include("community/core-periphery.jl") include("community/clustering.jl") include("community/cliques.jl") @@ -558,6 +579,8 @@ include("vertexcover/random_vertex_cover.jl") include("Experimental/Experimental.jl") include("Parallel/Parallel.jl") include("Test/Test.jl") +include("planarity.jl") +include("spanningtrees/planar_maximally_filtered_graph.jl") using .LinAlg end # module diff --git a/src/Parallel/Parallel.jl b/src/Parallel/Parallel.jl index 6dc931231..3917dde96 100644 --- a/src/Parallel/Parallel.jl +++ b/src/Parallel/Parallel.jl @@ -2,9 +2,7 @@ module Parallel using Graphs using Graphs: sample, AbstractPathState, JohnsonState, BellmanFordState, FloydWarshallState -using Distributed: @distributed using Base.Threads: @threads, nthreads, Atomic, atomic_add!, atomic_cas! -using SharedArrays: SharedMatrix, SharedVector, sdata using ArnoldiMethod: LM, SR, LR, partialschur, partialeigen using Random: AbstractRNG, shuffle import SparseArrays: sparse diff --git a/src/Parallel/centrality/betweenness.jl b/src/Parallel/centrality/betweenness.jl index 14a7a5a8a..77bd739dc 100644 --- a/src/Parallel/centrality/betweenness.jl +++ b/src/Parallel/centrality/betweenness.jl @@ -4,7 +4,7 @@ function betweenness_centrality( distmx::AbstractMatrix=weights(g); normalize=true, endpoints=false, - parallel=:distributed, + parallel=:threads, ) return if parallel == :distributed distr_betweenness_centrality( @@ -23,7 +23,7 @@ function betweenness_centrality( distmx::AbstractMatrix=weights(g); normalize=true, endpoints=false, - parallel=:distributed, + parallel=:threads, rng::Union{Nothing,AbstractRNG}=nothing, seed::Union{Nothing,Integer}=nothing, ) @@ -39,37 +39,10 @@ function betweenness_centrality( end end -function distr_betweenness_centrality( - g::AbstractGraph, - vs=vertices(g), - distmx::AbstractMatrix=weights(g); - normalize=true, - endpoints=false, -)::Vector{Float64} - n_v = nv(g) - k = length(vs) - isdir = is_directed(g) - - # Parallel reduction - - betweenness = @distributed (+) for s in vs - temp_betweenness = zeros(n_v) - if degree(g, s) > 0 # this might be 1? - state = Graphs.dijkstra_shortest_paths( - g, s, distmx; allpaths=true, trackvertices=true - ) - if endpoints - Graphs._accumulate_endpoints!(temp_betweenness, state, g, s) - else - Graphs._accumulate_basic!(temp_betweenness, state, g, s) - end - end - temp_betweenness - end - - Graphs._rescale!(betweenness, n_v, normalize, isdir, k) - - return betweenness +function distr_betweenness_centrality(args...; kwargs...) + return error( + "`parallel = :distributed` requested, but SharedArrays or Distributed is not loaded" + ) end function distr_betweenness_centrality( diff --git a/src/Parallel/centrality/closeness.jl b/src/Parallel/centrality/closeness.jl index 920421211..2259ea0aa 100644 --- a/src/Parallel/centrality/closeness.jl +++ b/src/Parallel/centrality/closeness.jl @@ -1,8 +1,5 @@ function closeness_centrality( - g::AbstractGraph, - distmx::AbstractMatrix=weights(g); - normalize=true, - parallel=:distributed, + g::AbstractGraph, distmx::AbstractMatrix=weights(g); normalize=true, parallel=:threads ) return if parallel == :distributed distr_closeness_centrality(g, distmx; normalize=normalize) @@ -11,33 +8,10 @@ function closeness_centrality( end end -function distr_closeness_centrality( - g::AbstractGraph, distmx::AbstractMatrix=weights(g); normalize=true -)::Vector{Float64} - n_v = Int(nv(g)) - closeness = SharedVector{Float64}(n_v) - fill!(closeness, 0.0) - - @sync @distributed for u in vertices(g) - if degree(g, u) == 0 # no need to do Dijkstra here - closeness[u] = 0.0 - else - d = Graphs.dijkstra_shortest_paths(g, u, distmx).dists - δ = filter(x -> x != typemax(x), d) - σ = sum(δ) - l = length(δ) - 1 - if σ > 0 - closeness[u] = l / σ - if normalize - n = l * 1.0 / (n_v - 1) - closeness[u] *= n - end - else - closeness[u] = 0.0 - end - end - end - return sdata(closeness) +function distr_closeness_centrality(args...; kwargs...) + return error( + "`parallel = :distributed` requested, but SharedArrays or Distributed is not loaded" + ) end function threaded_closeness_centrality( diff --git a/src/Parallel/centrality/radiality.jl b/src/Parallel/centrality/radiality.jl index e10fbe81b..ace98d454 100644 --- a/src/Parallel/centrality/radiality.jl +++ b/src/Parallel/centrality/radiality.jl @@ -1,4 +1,4 @@ -function radiality_centrality(g::AbstractGraph; parallel=:distributed) +function radiality_centrality(g::AbstractGraph; parallel=:threads) return if parallel == :distributed distr_radiality_centrality(g) else @@ -6,22 +6,10 @@ function radiality_centrality(g::AbstractGraph; parallel=:distributed) end end -function distr_radiality_centrality(g::AbstractGraph)::Vector{Float64} - n_v = nv(g) - vs = vertices(g) - n = ne(g) - meandists = SharedVector{Float64}(Int(n_v)) - maxdists = SharedVector{Float64}(Int(n_v)) - - @sync @distributed for i in 1:n_v - d = Graphs.dijkstra_shortest_paths(g, vs[i]) - maxdists[i] = maximum(d.dists) - meandists[i] = sum(d.dists) / (n_v - 1) - nothing - end - dmtr = maximum(maxdists) - radialities = collect(meandists) - return ((dmtr + 1) .- radialities) ./ dmtr +function distr_radiality_centrality(args...; kwargs...) + return error( + "`parallel = :distributed` requested, but SharedArrays or Distributed is not loaded" + ) end function threaded_radiality_centrality(g::AbstractGraph)::Vector{Float64} diff --git a/src/Parallel/centrality/stress.jl b/src/Parallel/centrality/stress.jl index 58844af8d..7d717aa11 100644 --- a/src/Parallel/centrality/stress.jl +++ b/src/Parallel/centrality/stress.jl @@ -1,4 +1,4 @@ -function stress_centrality(g::AbstractGraph, vs=vertices(g); parallel=:distributed) +function stress_centrality(g::AbstractGraph, vs=vertices(g); parallel=:threads) return if parallel == :distributed distr_stress_centrality(g, vs) else @@ -9,7 +9,7 @@ end function stress_centrality( g::AbstractGraph, k::Integer; - parallel=:distributed, + parallel=:threads, rng::Union{Nothing,AbstractRNG}=nothing, seed::Union{Nothing,Integer}=nothing, ) @@ -21,21 +21,10 @@ function stress_centrality( end end -function distr_stress_centrality(g::AbstractGraph, vs=vertices(g))::Vector{Int64} - n_v = nv(g) - k = length(vs) - isdir = is_directed(g) - - # Parallel reduction - stress = @distributed (+) for s in vs - temp_stress = zeros(Int64, n_v) - if degree(g, s) > 0 # this might be 1? - state = Graphs.dijkstra_shortest_paths(g, s; allpaths=true, trackvertices=true) - Graphs._stress_accumulate_basic!(temp_stress, state, g, s) - end - temp_stress - end - return stress +function distr_stress_centrality(args...; kwargs...) + return error( + "`parallel = :distributed` requested, but SharedArrays or Distributed is not loaded" + ) end function threaded_stress_centrality(g::AbstractGraph, vs=vertices(g))::Vector{Int64} diff --git a/src/Parallel/distance.jl b/src/Parallel/distance.jl index 3145339f3..5e5bf5c13 100644 --- a/src/Parallel/distance.jl +++ b/src/Parallel/distance.jl @@ -1,20 +1,45 @@ # used in shortest path calculations function eccentricity( + g::AbstractGraph, + vs=vertices(g), + distmx::AbstractMatrix{T}=weights(g); + parallel::Symbol=:threads, +) where {T<:Number} + return if parallel === :threads + threaded_eccentricity(g, vs, distmx) + elseif parallel === :distributed + distr_eccentricity(g, vs, distmx) + else + throw( + ArgumentError( + "Unsupported parallel argument '$(repr(parallel))' (supported: ':threads' or ':distributed')", + ), + ) + end +end + +function distr_eccentricity(args...; kwargs...) + return error( + "`parallel = :distributed` requested, but SharedArrays or Distributed is not loaded" + ) +end + +function threaded_eccentricity( g::AbstractGraph, vs=vertices(g), distmx::AbstractMatrix{T}=weights(g) ) where {T<:Number} vlen = length(vs) - eccs = SharedVector{T}(vlen) - @sync @distributed for i in 1:vlen - eccs[i] = maximum(Graphs.dijkstra_shortest_paths(g, vs[i], distmx).dists) + eccs = Vector{T}(undef, vlen) + Base.Threads.@threads for i in 1:vlen + d = Graphs.dijkstra_shortest_paths(g, vs[i], distmx) + eccs[i] = maximum(d.dists) end - d = sdata(eccs) - maximum(d) == typemax(T) && @warn("Infinite path length detected") - return d + maximum(eccs) == typemax(T) && @warn("Infinite path length detected") + return eccs end -function eccentricity(g::AbstractGraph, distmx::AbstractMatrix) - return eccentricity(g, vertices(g), distmx) +function eccentricity(g::AbstractGraph, distmx::AbstractMatrix; parallel::Symbol=:threads) + return eccentricity(g, vertices(g), distmx; parallel) end function diameter(g::AbstractGraph, distmx::AbstractMatrix=weights(g)) diff --git a/src/Parallel/shortestpaths/dijkstra.jl b/src/Parallel/shortestpaths/dijkstra.jl index 70363cf93..4aa6804e3 100644 --- a/src/Parallel/shortestpaths/dijkstra.jl +++ b/src/Parallel/shortestpaths/dijkstra.jl @@ -9,29 +9,55 @@ struct MultipleDijkstraState{T<:Number,U<:Integer} <: AbstractPathState end """ - Parallel.dijkstra_shortest_paths(g, sources=vertices(g), distmx=weights(g)) + Parallel.dijkstra_shortest_paths(g, sources=vertices(g), distmx=weights(g), parallel=:distributed) Compute the shortest paths between all pairs of vertices in graph `g` by running [`dijkstra_shortest_paths`] for every vertex and using an optional list of source vertex `sources` and an optional distance matrix `distmx`. Return a [`Parallel.MultipleDijkstraState`](@ref) with relevant -traversal information. +traversal information. The `parallel` argument can be set to `:threads` or `:distributed` for multi- +threaded or multi-process parallelism, respectively. """ function dijkstra_shortest_paths( + g::AbstractGraph{U}, + sources=vertices(g), + distmx::AbstractMatrix{T}=weights(g); + parallel::Symbol=:threads, +) where {T<:Number} where {U} + return if parallel === :threads + threaded_dijkstra_shortest_paths(g, sources, distmx) + elseif parallel === :distributed + distr_dijkstra_shortest_paths(g, sources, distmx) + else + throw( + ArgumentError( + "Unsupported parallel argument '$(repr(parallel))' (supported: ':threads' or ':distributed')", + ), + ) + end +end + +function threaded_dijkstra_shortest_paths( g::AbstractGraph{U}, sources=vertices(g), distmx::AbstractMatrix{T}=weights(g) ) where {T<:Number} where {U} n_v = nv(g) r_v = length(sources) # TODO: remove `Int` once julialang/#23029 / #23032 are resolved - dists = SharedMatrix{T}(Int(r_v), Int(n_v)) - parents = SharedMatrix{U}(Int(r_v), Int(n_v)) + dists = Matrix{T}(undef, Int(r_v), Int(n_v)) + parents = Matrix{U}(undef, Int(r_v), Int(n_v)) - @sync @distributed for i in 1:r_v + Base.Threads.@threads for i in 1:r_v state = Graphs.dijkstra_shortest_paths(g, sources[i], distmx) dists[i, :] = state.dists parents[i, :] = state.parents end - result = MultipleDijkstraState(sdata(dists), sdata(parents)) + result = MultipleDijkstraState(dists, parents) return result end + +function distr_dijkstra_shortest_paths(args...; kwargs...) + return error( + "`parallel = :distributed` requested, but SharedArrays or Distributed is not loaded" + ) +end diff --git a/src/Parallel/traversals/greedy_color.jl b/src/Parallel/traversals/greedy_color.jl index 6eb0c1e8e..1b6d7a430 100644 --- a/src/Parallel/traversals/greedy_color.jl +++ b/src/Parallel/traversals/greedy_color.jl @@ -1,18 +1,42 @@ -function random_greedy_color(g::AbstractGraph{T}, reps::Integer) where {T<:Integer} - best = @distributed (Graphs.best_color) for i in 1:reps +function random_greedy_color( + g::AbstractGraph{T}, reps::Integer; parallel::Symbol=:threads +) where {T<:Integer} + return if parallel === :threads + threaded_random_greedy_color(g, reps) + elseif parallel === :distributed + distr_random_greedy_color(g, reps) + else + throw( + ArgumentError( + "Unsupported parallel argument '$(repr(parallel))' (supported: ':threads' or ':distributed')", + ), + ) + end +end + +function threaded_random_greedy_color(g::AbstractGraph{T}, reps::Integer) where {T<:Integer} + local_best = Vector{Graphs.Coloring{T}}(undef, reps) + Base.Threads.@threads for i in 1:reps seq = shuffle(vertices(g)) - Graphs.perm_greedy_color(g, seq) + local_best[i] = Graphs.perm_greedy_color(g, seq) end + best = reduce(Graphs.best_color, local_best) return convert(Graphs.Coloring{T}, best) end +function distr_random_greedy_color(args...; kwargs...) + return error( + "`parallel = :distributed` requested, but SharedArrays or Distributed is not loaded" + ) +end + function greedy_color( - g::AbstractGraph{U}; sort_degree::Bool=false, reps::Integer=1 + g::AbstractGraph{U}; sort_degree::Bool=false, reps::Integer=1, parallel::Symbol=:threads ) where {U<:Integer} return if sort_degree Graphs.degree_greedy_color(g) else - Parallel.random_greedy_color(g, reps) + Parallel.random_greedy_color(g, reps; parallel) end end diff --git a/src/Parallel/utils.jl b/src/Parallel/utils.jl index fe763ecdc..85847db33 100644 --- a/src/Parallel/utils.jl +++ b/src/Parallel/utils.jl @@ -22,14 +22,10 @@ end Distributed implementation of [`generate_reduce`](@ref). """ -function distr_generate_reduce( - g::AbstractGraph{T}, gen_func::Function, comp::Comp, reps::Integer -) where {T<:Integer,Comp} - # Type assert required for type stability - min_set::Vector{T} = @distributed ((x, y) -> comp(x, y) ? x : y) for _ in 1:reps - gen_func(g) - end - return min_set +function distr_generate_reduce(args...; kwargs...) + return error( + "`parallel = :distributed` requested, but SharedArrays or Distributed is not loaded" + ) end """ diff --git a/src/SimpleGraphs/SimpleGraphs.jl b/src/SimpleGraphs/SimpleGraphs.jl index 7c2589d7f..b7b8f84ca 100644 --- a/src/SimpleGraphs/SimpleGraphs.jl +++ b/src/SimpleGraphs/SimpleGraphs.jl @@ -15,6 +15,7 @@ import Graphs: AbstractGraph, AbstractEdge, AbstractEdgeIter, + FrozenVector, src, dst, edgetype, @@ -93,6 +94,7 @@ export AbstractSimpleGraph, cycle_digraph, binary_tree, double_binary_tree, + regular_tree, roach_graph, clique_graph, barbell_graph, @@ -152,8 +154,8 @@ add_edge!(g::AbstractSimpleGraph, x) = add_edge!(g, edgetype(g)(x)) has_edge(g::AbstractSimpleGraph, x, y) = has_edge(g, edgetype(g)(x, y)) add_edge!(g::AbstractSimpleGraph, x, y) = add_edge!(g, edgetype(g)(x, y)) -inneighbors(g::AbstractSimpleGraph, v::Integer) = badj(g, v) -outneighbors(g::AbstractSimpleGraph, v::Integer) = fadj(g, v) +inneighbors(g::AbstractSimpleGraph, v::Integer) = FrozenVector(badj(g, v)) +outneighbors(g::AbstractSimpleGraph, v::Integer) = FrozenVector(fadj(g, v)) function issubset(g::T, h::T) where {T<:AbstractSimpleGraph} nv(g) <= nv(h) || return false diff --git a/src/SimpleGraphs/generators/randgraphs.jl b/src/SimpleGraphs/generators/randgraphs.jl index 750e22b5a..0f2a04468 100644 --- a/src/SimpleGraphs/generators/randgraphs.jl +++ b/src/SimpleGraphs/generators/randgraphs.jl @@ -1164,7 +1164,7 @@ function stochastic_block_model( for b in a:K ((a == b) && !(c[a, b] <= n[b] - 1)) || ((a != b) && !(c[a, b] <= n[b])) && error( - "Mean degree cannot be greater than available neighbors in the block.", + "Mean degree cannot be greater than available neighbors in the block." ) # TODO 0.7: turn into some other error? m = a == b ? div(n[a] * (n[a] - 1), 2) : n[a] * n[b] diff --git a/src/SimpleGraphs/generators/smallgraphs.jl b/src/SimpleGraphs/generators/smallgraphs.jl index 0706d5d81..60a846711 100644 --- a/src/SimpleGraphs/generators/smallgraphs.jl +++ b/src/SimpleGraphs/generators/smallgraphs.jl @@ -81,179 +81,173 @@ diamond_graph() = SimpleGraph(SimpleEdge.([(1, 2), (1, 3), (2, 3), (2, 4), (3, 4 bull_graph() = SimpleGraph(SimpleEdge.([(1, 2), (1, 3), (2, 3), (2, 4), (3, 5)])) function chvatal_graph() - e = - SimpleEdge.([ - (1, 2), - (1, 5), - (1, 7), - (1, 10), - (2, 3), - (2, 6), - (2, 8), - (3, 4), - (3, 7), - (3, 9), - (4, 5), - (4, 8), - (4, 10), - (5, 6), - (5, 9), - (6, 11), - (6, 12), - (7, 11), - (7, 12), - (8, 9), - (8, 12), - (9, 11), - (10, 11), - (10, 12), - ]) + e = SimpleEdge.([ + (1, 2), + (1, 5), + (1, 7), + (1, 10), + (2, 3), + (2, 6), + (2, 8), + (3, 4), + (3, 7), + (3, 9), + (4, 5), + (4, 8), + (4, 10), + (5, 6), + (5, 9), + (6, 11), + (6, 12), + (7, 11), + (7, 12), + (8, 9), + (8, 12), + (9, 11), + (10, 11), + (10, 12), + ]) return SimpleGraph(e) end function cubical_graph() - e = - SimpleEdge.([ - (1, 2), - (1, 4), - (1, 5), - (2, 3), - (2, 8), - (3, 4), - (3, 7), - (4, 6), - (5, 6), - (5, 8), - (6, 7), - (7, 8), - ]) + e = SimpleEdge.([ + (1, 2), + (1, 4), + (1, 5), + (2, 3), + (2, 8), + (3, 4), + (3, 7), + (4, 6), + (5, 6), + (5, 8), + (6, 7), + (7, 8), + ]) return SimpleGraph(e) end function desargues_graph() - e = - SimpleEdge.([ - (1, 2), - (1, 6), - (1, 20), - (2, 3), - (2, 17), - (3, 4), - (3, 12), - (4, 5), - (4, 15), - (5, 6), - (5, 10), - (6, 7), - (7, 8), - (7, 16), - (8, 9), - (8, 19), - (9, 10), - (9, 14), - (10, 11), - (11, 12), - (11, 20), - (12, 13), - (13, 14), - (13, 18), - (14, 15), - (15, 16), - (16, 17), - (17, 18), - (18, 19), - (19, 20), - ]) + e = SimpleEdge.([ + (1, 2), + (1, 6), + (1, 20), + (2, 3), + (2, 17), + (3, 4), + (3, 12), + (4, 5), + (4, 15), + (5, 6), + (5, 10), + (6, 7), + (7, 8), + (7, 16), + (8, 9), + (8, 19), + (9, 10), + (9, 14), + (10, 11), + (11, 12), + (11, 20), + (12, 13), + (13, 14), + (13, 18), + (14, 15), + (15, 16), + (16, 17), + (17, 18), + (18, 19), + (19, 20), + ]) return SimpleGraph(e) end function dodecahedral_graph() - e = - SimpleEdge.([ - (1, 2), - (1, 11), - (1, 20), - (2, 3), - (2, 9), - (3, 4), - (3, 7), - (4, 5), - (4, 20), - (5, 6), - (5, 18), - (6, 7), - (6, 16), - (7, 8), - (8, 9), - (8, 15), - (9, 10), - (10, 11), - (10, 14), - (11, 12), - (12, 13), - (12, 19), - (13, 14), - (13, 17), - (14, 15), - (15, 16), - (16, 17), - (17, 18), - (18, 19), - (19, 20), - ]) + e = SimpleEdge.([ + (1, 2), + (1, 11), + (1, 20), + (2, 3), + (2, 9), + (3, 4), + (3, 7), + (4, 5), + (4, 20), + (5, 6), + (5, 18), + (6, 7), + (6, 16), + (7, 8), + (8, 9), + (8, 15), + (9, 10), + (10, 11), + (10, 14), + (11, 12), + (12, 13), + (12, 19), + (13, 14), + (13, 17), + (14, 15), + (15, 16), + (16, 17), + (17, 18), + (18, 19), + (19, 20), + ]) return SimpleGraph(e) end function frucht_graph() - e = - SimpleEdge.([ - (1, 2), - (1, 7), - (1, 8), - (2, 3), - (2, 8), - (3, 4), - (3, 9), - (4, 5), - (4, 10), - (5, 6), - (5, 10), - (6, 7), - (6, 11), - (7, 11), - (8, 12), - (9, 10), - (9, 12), - (11, 12), - ]) + e = SimpleEdge.([ + (1, 2), + (1, 7), + (1, 8), + (2, 3), + (2, 8), + (3, 4), + (3, 9), + (4, 5), + (4, 10), + (5, 6), + (5, 10), + (6, 7), + (6, 11), + (7, 11), + (8, 12), + (9, 10), + (9, 12), + (11, 12), + ]) return SimpleGraph(e) end function heawood_graph() - e = - SimpleEdge.([ - (1, 2), - (1, 6), - (1, 14), - (2, 3), - (2, 11), - (3, 4), - (3, 8), - (4, 5), - (4, 13), - (5, 6), - (5, 10), - (6, 7), - (7, 8), - (7, 12), - (8, 9), - (9, 10), - (9, 14), - (10, 11), - (11, 12), - (12, 13), - (13, 14), - ]) + e = SimpleEdge.([ + (1, 2), + (1, 6), + (1, 14), + (2, 3), + (2, 11), + (3, 4), + (3, 8), + (4, 5), + (4, 13), + (5, 6), + (5, 10), + (6, 7), + (7, 8), + (7, 12), + (8, 9), + (9, 10), + (9, 14), + (10, 11), + (11, 12), + (12, 13), + (13, 14), + ]) return SimpleGraph(e) end @@ -270,263 +264,255 @@ function house_x_graph() end function icosahedral_graph() - e = - SimpleEdge.([ - (1, 2), - (1, 6), - (1, 8), - (1, 9), - (1, 12), - (2, 3), - (2, 6), - (2, 7), - (2, 9), - (3, 4), - (3, 7), - (3, 9), - (3, 10), - (4, 5), - (4, 7), - (4, 10), - (4, 11), - (5, 6), - (5, 7), - (5, 11), - (5, 12), - (6, 7), - (6, 12), - (8, 9), - (8, 10), - (8, 11), - (8, 12), - (9, 10), - (10, 11), - (11, 12), - ]) + e = SimpleEdge.([ + (1, 2), + (1, 6), + (1, 8), + (1, 9), + (1, 12), + (2, 3), + (2, 6), + (2, 7), + (2, 9), + (3, 4), + (3, 7), + (3, 9), + (3, 10), + (4, 5), + (4, 7), + (4, 10), + (4, 11), + (5, 6), + (5, 7), + (5, 11), + (5, 12), + (6, 7), + (6, 12), + (8, 9), + (8, 10), + (8, 11), + (8, 12), + (9, 10), + (10, 11), + (11, 12), + ]) return SimpleGraph(e) end function karate_graph() - e = - SimpleEdge.([ - (1, 2), - (1, 3), - (1, 4), - (1, 5), - (1, 6), - (1, 7), - (1, 8), - (1, 9), - (1, 11), - (1, 12), - (1, 13), - (1, 14), - (1, 18), - (1, 20), - (1, 22), - (1, 32), - (2, 3), - (2, 4), - (2, 8), - (2, 14), - (2, 18), - (2, 20), - (2, 22), - (2, 31), - (3, 4), - (3, 8), - (3, 9), - (3, 10), - (3, 14), - (3, 28), - (3, 29), - (3, 33), - (4, 8), - (4, 13), - (4, 14), - (5, 7), - (5, 11), - (6, 7), - (6, 11), - (6, 17), - (7, 17), - (9, 31), - (9, 33), - (9, 34), - (10, 34), - (14, 34), - (15, 33), - (15, 34), - (16, 33), - (16, 34), - (19, 33), - (19, 34), - (20, 34), - (21, 33), - (21, 34), - (23, 33), - (23, 34), - (24, 26), - (24, 28), - (24, 30), - (24, 33), - (24, 34), - (25, 26), - (25, 28), - (25, 32), - (26, 32), - (27, 30), - (27, 34), - (28, 34), - (29, 32), - (29, 34), - (30, 33), - (30, 34), - (31, 33), - (31, 34), - (32, 33), - (32, 34), - (33, 34), - ]) + e = SimpleEdge.([ + (1, 2), + (1, 3), + (1, 4), + (1, 5), + (1, 6), + (1, 7), + (1, 8), + (1, 9), + (1, 11), + (1, 12), + (1, 13), + (1, 14), + (1, 18), + (1, 20), + (1, 22), + (1, 32), + (2, 3), + (2, 4), + (2, 8), + (2, 14), + (2, 18), + (2, 20), + (2, 22), + (2, 31), + (3, 4), + (3, 8), + (3, 9), + (3, 10), + (3, 14), + (3, 28), + (3, 29), + (3, 33), + (4, 8), + (4, 13), + (4, 14), + (5, 7), + (5, 11), + (6, 7), + (6, 11), + (6, 17), + (7, 17), + (9, 31), + (9, 33), + (9, 34), + (10, 34), + (14, 34), + (15, 33), + (15, 34), + (16, 33), + (16, 34), + (19, 33), + (19, 34), + (20, 34), + (21, 33), + (21, 34), + (23, 33), + (23, 34), + (24, 26), + (24, 28), + (24, 30), + (24, 33), + (24, 34), + (25, 26), + (25, 28), + (25, 32), + (26, 32), + (27, 30), + (27, 34), + (28, 34), + (29, 32), + (29, 34), + (30, 33), + (30, 34), + (31, 33), + (31, 34), + (32, 33), + (32, 34), + (33, 34), + ]) return SimpleGraph(e) end function krackhardt_kite_graph() - e = - SimpleEdge.([ - (1, 2), - (1, 3), - (1, 4), - (1, 6), - (2, 4), - (2, 5), - (2, 7), - (3, 4), - (3, 6), - (4, 5), - (4, 6), - (4, 7), - (5, 7), - (6, 7), - (6, 8), - (7, 8), - (8, 9), - (9, 10), - ]) + e = SimpleEdge.([ + (1, 2), + (1, 3), + (1, 4), + (1, 6), + (2, 4), + (2, 5), + (2, 7), + (3, 4), + (3, 6), + (4, 5), + (4, 6), + (4, 7), + (5, 7), + (6, 7), + (6, 8), + (7, 8), + (8, 9), + (9, 10), + ]) return SimpleGraph(e) end function moebius_kantor_graph() - e = - SimpleEdge.([ - (1, 2), - (1, 6), - (1, 16), - (2, 3), - (2, 13), - (3, 4), - (3, 8), - (4, 5), - (4, 15), - (5, 6), - (5, 10), - (6, 7), - (7, 8), - (7, 12), - (8, 9), - (9, 10), - (9, 14), - (10, 11), - (11, 12), - (11, 16), - (12, 13), - (13, 14), - (14, 15), - (15, 16), - ]) + e = SimpleEdge.([ + (1, 2), + (1, 6), + (1, 16), + (2, 3), + (2, 13), + (3, 4), + (3, 8), + (4, 5), + (4, 15), + (5, 6), + (5, 10), + (6, 7), + (7, 8), + (7, 12), + (8, 9), + (9, 10), + (9, 14), + (10, 11), + (11, 12), + (11, 16), + (12, 13), + (13, 14), + (14, 15), + (15, 16), + ]) return SimpleGraph(e) end function octahedral_graph() - e = - SimpleEdge.([ - (1, 2), - (1, 3), - (1, 4), - (1, 5), - (2, 3), - (2, 4), - (2, 6), - (3, 5), - (3, 6), - (4, 5), - (4, 6), - (5, 6), - ]) + e = SimpleEdge.([ + (1, 2), + (1, 3), + (1, 4), + (1, 5), + (2, 3), + (2, 4), + (2, 6), + (3, 5), + (3, 6), + (4, 5), + (4, 6), + (5, 6), + ]) return SimpleGraph(e) end function pappus_graph() - e = - SimpleEdge.([ - (1, 2), - (1, 6), - (1, 18), - (2, 3), - (2, 9), - (3, 4), - (3, 14), - (4, 5), - (4, 11), - (5, 6), - (5, 16), - (6, 7), - (7, 8), - (7, 12), - (8, 9), - (8, 15), - (9, 10), - (10, 11), - (10, 17), - (11, 12), - (12, 13), - (13, 14), - (13, 18), - (14, 15), - (15, 16), - (16, 17), - (17, 18), - ]) + e = SimpleEdge.([ + (1, 2), + (1, 6), + (1, 18), + (2, 3), + (2, 9), + (3, 4), + (3, 14), + (4, 5), + (4, 11), + (5, 6), + (5, 16), + (6, 7), + (7, 8), + (7, 12), + (8, 9), + (8, 15), + (9, 10), + (10, 11), + (10, 17), + (11, 12), + (12, 13), + (13, 14), + (13, 18), + (14, 15), + (15, 16), + (16, 17), + (17, 18), + ]) return SimpleGraph(e) end function petersen_graph() - e = - SimpleEdge.([ - (1, 2), - (1, 5), - (1, 6), - (2, 3), - (2, 7), - (3, 4), - (3, 8), - (4, 5), - (4, 9), - (5, 10), - (6, 8), - (6, 9), - (7, 9), - (7, 10), - (8, 10), - ]) + e = SimpleEdge.([ + (1, 2), + (1, 5), + (1, 6), + (2, 3), + (2, 7), + (3, 4), + (3, 8), + (4, 5), + (4, 9), + (5, 10), + (6, 8), + (6, 9), + (7, 9), + (7, 10), + (8, 10), + ]) return SimpleGraph(e) end function sedgewick_maze_graph() - e = - SimpleEdge.([ - (1, 3), (1, 6), (1, 8), (2, 8), (3, 7), (4, 5), (4, 6), (5, 6), (5, 7), (5, 8) - ]) + e = SimpleEdge.([ + (1, 3), (1, 6), (1, 8), (2, 8), (3, 7), (4, 5), (4, 6), (5, 6), (5, 7), (5, 8) + ]) return SimpleGraph(e) end @@ -535,170 +521,166 @@ function tetrahedral_graph() end function truncated_cube_graph() - e = - SimpleEdge.([ - (1, 2), - (1, 3), - (1, 5), - (2, 12), - (2, 15), - (3, 4), - (3, 5), - (4, 7), - (4, 9), - (5, 6), - (6, 17), - (6, 19), - (7, 8), - (7, 9), - (8, 11), - (8, 13), - (9, 10), - (10, 18), - (10, 21), - (11, 12), - (11, 13), - (12, 15), - (13, 14), - (14, 22), - (14, 23), - (15, 16), - (16, 20), - (16, 24), - (17, 18), - (17, 19), - (18, 21), - (19, 20), - (20, 24), - (21, 22), - (22, 23), - (23, 24), - ]) + e = SimpleEdge.([ + (1, 2), + (1, 3), + (1, 5), + (2, 12), + (2, 15), + (3, 4), + (3, 5), + (4, 7), + (4, 9), + (5, 6), + (6, 17), + (6, 19), + (7, 8), + (7, 9), + (8, 11), + (8, 13), + (9, 10), + (10, 18), + (10, 21), + (11, 12), + (11, 13), + (12, 15), + (13, 14), + (14, 22), + (14, 23), + (15, 16), + (16, 20), + (16, 24), + (17, 18), + (17, 19), + (18, 21), + (19, 20), + (20, 24), + (21, 22), + (22, 23), + (23, 24), + ]) return SimpleGraph(e) end function truncated_tetrahedron_graph() - e = - SimpleEdge.([ - (1, 2), - (1, 3), - (1, 10), - (2, 3), - (2, 7), - (3, 4), - (4, 5), - (4, 12), - (5, 6), - (5, 12), - (6, 7), - (6, 8), - (7, 8), - (8, 9), - (9, 10), - (9, 11), - (10, 11), - (11, 12), - ]) + e = SimpleEdge.([ + (1, 2), + (1, 3), + (1, 10), + (2, 3), + (2, 7), + (3, 4), + (4, 5), + (4, 12), + (5, 6), + (5, 12), + (6, 7), + (6, 8), + (7, 8), + (8, 9), + (9, 10), + (9, 11), + (10, 11), + (11, 12), + ]) return SimpleGraph(e) end function truncated_tetrahedron_digraph() - e = - SimpleEdge.([ - (1, 2), - (1, 3), - (1, 10), - (2, 3), - (2, 7), - (3, 4), - (4, 5), - (4, 12), - (5, 6), - (5, 12), - (6, 7), - (6, 8), - (7, 8), - (8, 9), - (9, 10), - (9, 11), - (10, 11), - (11, 12), - ]) + e = SimpleEdge.([ + (1, 2), + (1, 3), + (1, 10), + (2, 3), + (2, 7), + (3, 4), + (4, 5), + (4, 12), + (5, 6), + (5, 12), + (6, 7), + (6, 8), + (7, 8), + (8, 9), + (9, 10), + (9, 11), + (10, 11), + (11, 12), + ]) return SimpleDiGraph(e) end function tutte_graph() - e = - SimpleEdge.([ - (1, 2), - (1, 3), - (1, 4), - (2, 5), - (2, 27), - (3, 11), - (3, 12), - (4, 19), - (4, 20), - (5, 6), - (5, 34), - (6, 7), - (6, 30), - (7, 8), - (7, 28), - (8, 9), - (8, 15), - (9, 10), - (9, 39), - (10, 11), - (10, 38), - (11, 40), - (12, 13), - (12, 40), - (13, 14), - (13, 36), - (14, 15), - (14, 16), - (15, 35), - (16, 17), - (16, 23), - (17, 18), - (17, 45), - (18, 19), - (18, 44), - (19, 46), - (20, 21), - (20, 46), - (21, 22), - (21, 42), - (22, 23), - (22, 24), - (23, 41), - (24, 25), - (24, 28), - (25, 26), - (25, 33), - (26, 27), - (26, 32), - (27, 34), - (28, 29), - (29, 30), - (29, 33), - (30, 31), - (31, 32), - (31, 34), - (32, 33), - (35, 36), - (35, 39), - (36, 37), - (37, 38), - (37, 40), - (38, 39), - (41, 42), - (41, 45), - (42, 43), - (43, 44), - (43, 46), - (44, 45), - ]) + e = SimpleEdge.([ + (1, 2), + (1, 3), + (1, 4), + (2, 5), + (2, 27), + (3, 11), + (3, 12), + (4, 19), + (4, 20), + (5, 6), + (5, 34), + (6, 7), + (6, 30), + (7, 8), + (7, 28), + (8, 9), + (8, 15), + (9, 10), + (9, 39), + (10, 11), + (10, 38), + (11, 40), + (12, 13), + (12, 40), + (13, 14), + (13, 36), + (14, 15), + (14, 16), + (15, 35), + (16, 17), + (16, 23), + (17, 18), + (17, 45), + (18, 19), + (18, 44), + (19, 46), + (20, 21), + (20, 46), + (21, 22), + (21, 42), + (22, 23), + (22, 24), + (23, 41), + (24, 25), + (24, 28), + (25, 26), + (25, 33), + (26, 27), + (26, 32), + (27, 34), + (28, 29), + (29, 30), + (29, 33), + (30, 31), + (31, 32), + (31, 34), + (32, 33), + (35, 36), + (35, 39), + (36, 37), + (37, 38), + (37, 40), + (38, 39), + (41, 42), + (41, 45), + (42, 43), + (43, 44), + (43, 46), + (44, 45), + ]) return SimpleGraph(e) end diff --git a/src/SimpleGraphs/generators/staticgraphs.jl b/src/SimpleGraphs/generators/staticgraphs.jl index 65fbb0bdb..e541a1c6b 100644 --- a/src/SimpleGraphs/generators/staticgraphs.jl +++ b/src/SimpleGraphs/generators/staticgraphs.jl @@ -530,12 +530,12 @@ function binary_tree(k::T) where {T<:Integer} fadjlist = Vector{Vector{T}}(undef, n) @inbounds fadjlist[1] = T[2, 3] @inbounds for i in 1:(k - 2) - @simd for j in (2^i):(2^(i + 1) - 1) + @simd for j in (2 ^ i):(2 ^ (i + 1) - 1) fadjlist[j] = T[j ÷ 2, 2j, 2j + 1] end end i = k - 1 - @inbounds @simd for j in (2^i):(2^(i + 1) - 1) + @inbounds @simd for j in (2 ^ i):(2 ^ (i + 1) - 1) fadjlist[j] = T[j ÷ 2] end return SimpleGraph(ne, fadjlist) @@ -568,6 +568,64 @@ function double_binary_tree(k::Integer) return g end +""" + regular_tree([T::Type], k, z) + +Create a regular tree or [perfect z-ary tree](https://en.wikipedia.org/wiki/M-ary_tree#Types_of_m-ary_trees): +a `k`-level tree where all nodes except the leaves have exactly `z` children. +For `z = 2` one recovers a binary tree. +The optional `T` argument specifies the element type, which defaults to `Int64`. + +# Examples +```jldoctest +julia> using Graphs + +julia> regular_tree(4, 3) +{40, 39} undirected simple Int64 graph + +julia> regular_tree(Int8, 3, 2) +{7, 6} undirected simple Int8 graph + +julia> regular_tree(5, 2) == binary_tree(5) +true +``` +""" +function regular_tree(T::Type{<:Integer}, k::Integer, z::Integer) + z <= 0 && throw(DomainError(z, "number of children must be positive")) + z == 1 && return path_graph(T(k)) + k <= 0 && return SimpleGraph(zero(T)) + k == 1 && return SimpleGraph(one(T)) + nbig = (BigInt(z)^k - 1) ÷ (z - 1) + if Graphs.isbounded(k) && nbig > typemax(T) + throw(InexactError(:convert, T, nbig)) + end + + n = T(nbig) + ne = n - 1 + fadjlist = Vector{Vector{T}}(undef, n) + @inbounds fadjlist[1] = convert.(T, 2:(z + 1)) + @inbounds for l in 2:(k - 1) + w = (z^(l - 1) - 1) ÷ (z - 1) + x = w + z^(l - 1) + @simd for i in 1:(z ^ (l - 1)) + j = w + i + fadjlist[j] = [ + T(ceil((j - x) / z) + w) + convert.(T, (x + (i - 1) * z + 1):(x + i * z)) + ] + end + end + l = k + w = (z^(l - 1) - 1) ÷ (z - 1) + x = w + z^(l - 1) + @inbounds @simd for j in (w + 1):x + fadjlist[j] = T[ceil((j - x) / z) + w] + end + return SimpleGraph(ne, fadjlist) +end + +regular_tree(k::Integer, z::Integer) = regular_tree(Int64, k, z) + """ roach_graph(k) diff --git a/src/SimpleGraphs/simpledigraph.jl b/src/SimpleGraphs/simpledigraph.jl index 598ed976b..0208c12c5 100644 --- a/src/SimpleGraphs/simpledigraph.jl +++ b/src/SimpleGraphs/simpledigraph.jl @@ -435,6 +435,15 @@ function ==(g::SimpleDiGraph, h::SimpleDiGraph) badj(g) == badj(h) end +function Base.hash(g::SimpleDiGraph, h::UInt) + r = hash(typeof(g), h) + r = hash(nv(g), r) + r = hash(ne(g), r) + r = hash(fadj(g), r) + r = hash(badj(g), r) + return r +end + is_directed(::Type{<:SimpleDiGraph}) = true function has_edge(g::SimpleDiGraph{T}, s, d) where {T} diff --git a/src/SimpleGraphs/simpleedgeiter.jl b/src/SimpleGraphs/simpleedgeiter.jl index 3c33fc95f..509319c68 100644 --- a/src/SimpleGraphs/simpleedgeiter.jl +++ b/src/SimpleGraphs/simpleedgeiter.jl @@ -30,7 +30,7 @@ eltype(::Type{SimpleEdgeIter{SimpleDiGraph{T}}}) where {T} = SimpleDiGraphEdge{T @traitfn @inline function iterate( eit::SimpleEdgeIter{G}, state=(one(eltype(eit.g)), 1) -) where {G <: AbstractSimpleGraph; !IsDirected{G}} +) where {G<:AbstractSimpleGraph;!IsDirected{G}} g = eit.g T = eltype(g) n = T(nv(g)) @@ -57,7 +57,7 @@ end @traitfn @inline function iterate( eit::SimpleEdgeIter{G}, state=(one(eltype(eit.g)), 1) -) where {G <: AbstractSimpleGraph; IsDirected{G}} +) where {G<:AbstractSimpleGraph;IsDirected{G}} g = eit.g T = eltype(g) n = T(nv(g)) diff --git a/src/SimpleGraphs/simplegraph.jl b/src/SimpleGraphs/simplegraph.jl index c97806548..0657d1277 100644 --- a/src/SimpleGraphs/simplegraph.jl +++ b/src/SimpleGraphs/simplegraph.jl @@ -271,7 +271,7 @@ end Construct a `SimpleGraph` from any `AbstractGraph` by enumerating edges. -If `g` is directed, a directed edge `{u, v}` is added if either directed edge `(u, v)` or `(v, u)` exists. +If `g` is directed, an undirected edge `{u, v}` is added if either directed edge `(u, v)` or `(v, u)` exists. """ function SimpleGraph{T}(g::AbstractGraph) where {T} eds = edges(g) @@ -430,6 +430,14 @@ function ==(g::SimpleGraph, h::SimpleGraph) return vertices(g) == vertices(h) && ne(g) == ne(h) && fadj(g) == fadj(h) end +function Base.hash(g::SimpleGraph, h::UInt) + r = hash(typeof(g), h) + r = hash(nv(g), r) + r = hash(ne(g), r) + r = hash(fadj(g), r) + return r +end + """ is_directed(g) diff --git a/src/biconnectivity/articulation.jl b/src/biconnectivity/articulation.jl index 3156e7a1a..a85864b0b 100644 --- a/src/biconnectivity/articulation.jl +++ b/src/biconnectivity/articulation.jl @@ -1,8 +1,9 @@ """ articulation(g) -Compute the [articulation points](https://en.wikipedia.org/wiki/Biconnected_component) -of a connected graph `g` and return an array containing all cut vertices. +Compute the [articulation points](https://en.wikipedia.org/wiki/Biconnected_component) (also +known as cut or seperating vertices) of an undirected graph `g` and return a vector +containing all the vertices of `g` that are articulation points. # Examples ```jldoctest @@ -22,74 +23,136 @@ julia> articulation(path_graph(5)) function articulation end @traitfn function articulation(g::AG::(!IsDirected)) where {T,AG<:AbstractGraph{T}} s = Vector{Tuple{T,T,T}}() - is_articulation_pt = falses(nv(g)) low = zeros(T, nv(g)) pre = zeros(T, nv(g)) + is_articulation_pt = falses(nv(g)) @inbounds for u in vertices(g) - pre[u] != 0 && continue - v = u - children = 0 - wi::T = zero(T) - w::T = zero(T) - cnt::T = one(T) - first_time = true - - # TODO the algorithm currently relies on the assumption that - # outneighbors(g, v) is indexable. This assumption might not be true - # in general, so in case that outneighbors does not produce a vector - # we collect these vertices. This might lead to a large number of - # allocations, so we should find a way to handle that case differently, - # or require inneighbors, outneighbors and neighbors to always - # return indexable collections. - - while !isempty(s) || first_time - first_time = false - if wi < 1 - pre[v] = cnt - cnt += 1 - low[v] = pre[v] - v_neighbors = collect_if_not_vector(outneighbors(g, v)) - wi = 1 - else - wi, u, v = pop!(s) - v_neighbors = collect_if_not_vector(outneighbors(g, v)) - w = v_neighbors[wi] - low[v] = min(low[v], low[w]) - if low[w] >= pre[v] && u != v + articulation_dfs!(is_articulation_pt, g, u, s, low, pre) + end + + articulation_points = T[v for (v, b) in enumerate(is_articulation_pt) if b] + + return articulation_points +end + +""" + is_articulation(g, v) + +Determine whether `v` is an +[articulation point](https://en.wikipedia.org/wiki/Biconnected_component) of an undirected +graph `g`, returning `true` if so and `false` otherwise. + +See also [`articulation`](@ref). + +# Examples +```jldoctest +julia> using Graphs + +julia> g = path_graph(5) +{5, 4} undirected simple Int64 graph + +julia> articulation(g) +3-element Vector{Int64}: + 2 + 3 + 4 + +julia> is_articulation(g, 2) +true + +julia> is_articulation(g, 1) +false +``` +""" +function is_articulation end +@traitfn function is_articulation(g::AG::(!IsDirected), v::T) where {T,AG<:AbstractGraph{T}} + s = Vector{Tuple{T,T,T}}() + low = zeros(T, nv(g)) + pre = zeros(T, nv(g)) + + return articulation_dfs!(nothing, g, v, s, low, pre) +end + +@traitfn function articulation_dfs!( + is_articulation_pt::Union{Nothing,BitVector}, + g::AG::(!IsDirected), + u::T, + s::Vector{Tuple{T,T,T}}, + low::Vector{T}, + pre::Vector{T}, +) where {T,AG<:AbstractGraph{T}} + if !isnothing(is_articulation_pt) + if pre[u] != 0 + return is_articulation_pt + end + end + + v = u + children = 0 + wi::T = zero(T) + w::T = zero(T) + cnt::T = one(T) + first_time = true + + # TODO the algorithm currently relies on the assumption that + # outneighbors(g, v) is indexable. This assumption might not be true + # in general, so in case that outneighbors does not produce a vector + # we collect these vertices. This might lead to a large number of + # allocations, so we should find a way to handle that case differently, + # or require inneighbors, outneighbors and neighbors to always + # return indexable collections. + + while !isempty(s) || first_time + first_time = false + if wi < 1 + pre[v] = cnt + cnt += 1 + low[v] = pre[v] + v_neighbors = collect_if_not_vector(outneighbors(g, v)) + wi = 1 + else + wi, u, v = pop!(s) + v_neighbors = collect_if_not_vector(outneighbors(g, v)) + w = v_neighbors[wi] + low[v] = min(low[v], low[w]) + if low[w] >= pre[v] && u != v + if isnothing(is_articulation_pt) + if v == u + return true + end + else is_articulation_pt[v] = true end - wi += 1 end - while wi <= length(v_neighbors) - w = v_neighbors[wi] - if pre[w] == 0 - if u == v - children += 1 - end - push!(s, (wi, u, v)) - wi = 0 - u = v - v = w - break - elseif w != u - low[v] = min(low[v], pre[w]) + wi += 1 + end + while wi <= length(v_neighbors) + w = v_neighbors[wi] + if pre[w] == 0 + if u == v + children += 1 end - wi += 1 + push!(s, (wi, u, v)) + wi = 0 + u = v + v = w + break + elseif w != u + low[v] = min(low[v], pre[w]) end - wi < 1 && continue + wi += 1 end + wi < 1 && continue + end - if children > 1 + if children > 1 + if isnothing(is_articulation_pt) + return u == v + else is_articulation_pt[u] = true end end - articulation_points = Vector{T}() - - for u in findall(is_articulation_pt) - push!(articulation_points, T(u)) - end - - return articulation_points + return isnothing(is_articulation_pt) ? false : is_articulation_pt end diff --git a/src/centrality/betweenness.jl b/src/centrality/betweenness.jl index 150a29493..0a2286f30 100644 --- a/src/centrality/betweenness.jl +++ b/src/centrality/betweenness.jl @@ -141,22 +141,15 @@ end function _rescale!( betweenness::Vector{Float64}, n::Integer, normalize::Bool, directed::Bool, k::Integer ) + scale = nothing if normalize - if n <= 2 - do_scale = false - else - do_scale = true + if 2 < n scale = 1.0 / ((n - 1) * (n - 2)) end - else - if !directed - do_scale = true - scale = 1.0 / 2.0 - else - do_scale = false - end + elseif !directed + scale = 1.0 / 2.0 end - if do_scale + if !isnothing(scale) if k > 0 scale = scale * n / k end diff --git a/src/community/clique_percolation.jl b/src/community/clique_percolation.jl index 84a8eebf1..7059de2ff 100644 --- a/src/community/clique_percolation.jl +++ b/src/community/clique_percolation.jl @@ -9,7 +9,7 @@ The parameter `k` defines the size of the clique to use in percolation. - [Palla G, Derenyi I, Farkas I J, et al.] (https://www.nature.com/articles/nature03607) # Examples -```jldoctest +```jldoctest; filter = r"^\\s+BitSet\\(\\[[\\d, ]+\\]\\)\$"m julia> using Graphs julia> clique_percolation(clique_graph(3, 2)) diff --git a/src/community/cliques.jl b/src/community/cliques.jl index ad1af2153..2f0b6c469 100644 --- a/src/community/cliques.jl +++ b/src/community/cliques.jl @@ -144,3 +144,125 @@ function maximal_cliques end end return cliques end + +""" + maximum_clique(g) + +Return a vector representing the node indices of a maximum clique +of the undirected graph `g`. + +```jldoctest +julia> using Graphs + +julia> maximum_clique(blockdiag(complete_graph(3), complete_graph(4))) +4-element Vector{Int64}: + 4 + 5 + 6 + 7 +``` +""" +function maximum_clique end +# see https://github.com/mauro3/SimpleTraits.jl/issues/47#issuecomment-327880153 for syntax +@traitfn function maximum_clique(g::AG::(!IsDirected)) where {T,AG<:AbstractGraph{T}} + return sort(argmax(length, maximal_cliques(g))) +end + +""" + clique_number(g) + +Returns the size of the largest clique of the undirected graph `g`. + +```jldoctest +julia> using Graphs + +julia> clique_number(blockdiag(complete_graph(3), complete_graph(4))) +4 +``` +""" +function clique_number end +# see https://github.com/mauro3/SimpleTraits.jl/issues/47#issuecomment-327880153 for syntax +@traitfn function clique_number(g::AG::(!IsDirected)) where {T,AG<:AbstractGraph{T}} + return maximum(length, maximal_cliques(g)) +end + +""" + maximal_independent_sets(g) + +Return a vector of vectors representing the node indices in each of the maximal +independent sets found in the undirected graph `g`. + +The graph will be converted to SimpleGraph at the start of the computation. + +```jldoctest; filter = r"^\\s+\\[[\\d, ]+\\]\$"m +julia> using Graphs + +julia> maximal_independent_sets(cycle_graph(5)) +5-element Vector{Vector{Int64}}: + [5, 2] + [5, 3] + [2, 4] + [1, 4] + [1, 3] +``` +""" +function maximal_independent_sets end +# see https://github.com/mauro3/SimpleTraits.jl/issues/47#issuecomment-327880153 for syntax +@traitfn function maximal_independent_sets( + g::AG::(!IsDirected) +) where {T,AG<:AbstractGraph{T}} + # Convert to SimpleGraph first because `complement` doesn't accept AbstractGraph. + return maximal_cliques(complement(SimpleGraph(g))) +end + +""" + maximum_independent_set(g) + +Return a vector representing the node indices of a maximum independent set +of the undirected graph `g`. + +The graph will be converted to SimpleGraph at the start of the computation. + +### See also +[`independent_set`](@ref) + +## Examples +```jldoctest; filter = r"^\\s+\\d+\$"m +julia> using Graphs + +julia> maximum_independent_set(cycle_graph(7)) +3-element Vector{Int64}: + 2 + 5 + 7 +``` +""" +function maximum_independent_set end +# see https://github.com/mauro3/SimpleTraits.jl/issues/47#issuecomment-327880153 for syntax +@traitfn function maximum_independent_set( + g::AG::(!IsDirected) +) where {T,AG<:AbstractGraph{T}} + # Convert to SimpleGraph first because `complement` doesn't accept AbstractGraph. + return maximum_clique(complement(SimpleGraph(g))) +end + +""" + independence_number(g) + +Returns the size of the largest independent set of the undirected graph `g`. + +The graph will be converted to SimpleGraph at the start of the computation. + +```jldoctest +julia> using Graphs + +julia> independence_number(cycle_graph(7)) +3 +``` +""" +function independence_number end +# see https://github.com/mauro3/SimpleTraits.jl/issues/47#issuecomment-327880153 for syntax +@traitfn function independence_number(g::AG::(!IsDirected)) where {T,AG<:AbstractGraph{T}} + # Convert to SimpleGraph first because `complement` doesn't accept AbstractGraph. + return clique_number(complement(SimpleGraph(g))) +end diff --git a/src/community/louvain.jl b/src/community/louvain.jl new file mode 100644 index 000000000..b424ee5e3 --- /dev/null +++ b/src/community/louvain.jl @@ -0,0 +1,271 @@ +""" + louvain(g, distmx=weights(g), γ=1; max_moves::Integer=1000, max_merges::Integer=1000, move_tol::Real=10e-10, merge_tol::Real=10e-10, rng=nothing, seed=nothing) + +Community detection using the louvain algorithm. Finds a partition of the vertices that +attempts to maximize the modularity. Returns a vector of community ids. + +### Optional Arguments +- `distmx=weights(g)`: distance matrix for weighted graphs +- `γ=1.0`: where `γ > 0` is a resolution parameter. Higher resolutions lead to more + communities, while lower resolutions lead to fewer communities. Where `γ=1.0` it + leads to the traditional definition of the modularity. +- `max_moves=1000`: maximum number of rounds moving vertices before merging. +- `max_merges=1000`: maximum number of merges. +- `move_tol=10e-10`: necessary increase of modularity to move a vertex. +- `merge_tol=10e-10`: necessary increase of modularity in the move stage to merge. +- `rng=nothing`: rng to use for reproducibility. May only pass one of rng or seed. +- `seed=nothing`: seed to use for reproducibility. May only pass one of rng or seed. + +### References +- [Vincent D Blondel et al J. Stat. Mech. (2008) P10008][https://doi.org/10.1088/1742-5468/2008/10/P10008] +- [Nicolas Dugué, Anthony Perez. Directed Louvain : maximizing modularity in directed networks.][https://hal.science/hal-01231784/document] + +# Examples +```jldoctest +julia> using Graphs + +julia> barbell = blockdiag(complete_graph(3), complete_graph(3)); + +julia> add_edge!(barbell, 1, 4); + +julia> louvain(barbell) +6-element Vector{Int64}: + 1 + 1 + 1 + 2 + 2 + 2 + +julia> louvain(barbell, γ=0.01) +6-element Vector{Int64}: + 1 + 1 + 1 + 1 + 1 + 1 +``` +""" +function louvain( + g::AbstractGraph{T}; + γ=1.0, + distmx::AbstractArray{<:Number}=weights(g), + max_moves::Integer=1000, + max_merges::Integer=1000, + move_tol::Real=10e-10, + merge_tol::Real=10e-10, + rng::Union{Nothing,AbstractRNG}=nothing, + seed::Union{Nothing,Integer}=nothing, +) where {T} + rng = rng_from_rng_or_seed(rng, seed) + n = nv(g) + if n == 0 + return T[] + end + + @debug "Running louvain with parameters γ=$(γ), max_moves=$(max_moves), " * + "max_merges=$(max_merges), move_tol=$(move_tol), merge_tol=$(merge_tol)" + + actual_coms = collect(one(T):nv(g)) + current_coms = copy(actual_coms) + # actual_coms is always of length nv(g) and holds the current com for each v in g + # current_coms is for the current graph; after merges it will be smaller than nv(g) + + for iter in 0:max_merges + current_modularity = modularity(g, current_coms; distmx=distmx, γ=γ) + @debug "Merge iteration $(iter). Current modularity is $(current_modularity)" + louvain_move!(g, γ, current_coms, rng, distmx, max_moves, move_tol) + # remap communities to 1-nc + com_map = Dict(old => new for (new, old) in enumerate(unique(current_coms))) + for i in eachindex(actual_coms) + actual_coms[i] = com_map[current_coms[actual_coms[i]]] + end + @debug "Communities after moving in iteration $(iter): $(actual_coms)" + for i in eachindex(current_coms) + current_coms[i] = com_map[current_coms[i]] + end + + # Stop if modularity gain is too small + new_modularity = modularity(g, current_coms; distmx=distmx, γ=γ) + @debug "New modularity is $(new_modularity) for a gain of $(new_modularity - + current_modularity)" + if new_modularity - current_modularity < merge_tol + break + end + g, distmx = louvain_merge(g, current_coms, distmx) + if nv(g) == 1 # nothing left to merge + break + end + current_coms = collect(one(T):nv(g)) + end + return actual_coms +end + +""" + louvain_move!(g, γ, c, rng, distmx=weights(g), max_moves=1000, move_tol=10e-10) + +The move stage of the louvain algorithm. +""" +function louvain_move!( + g, γ, c, rng, distmx=weights(g), max_moves::Integer=1000, move_tol::Real=10e-10 +) + vertex_order = shuffle!(rng, collect(vertices(g))) + nc = maximum(c) + + # Compute graph and community volumes + m = 0 + c_vols = zeros(eltype(distmx), ((is_directed(g) ? 2 : 1), nc)) + # if is_directed use row 1 for in and 2 for out + for e in edges(g) + m += distmx[src(e), dst(e)] + c_vols[1, c[src(e)]] += distmx[src(e), dst(e)] + if is_directed(g) + c_vols[2, c[dst(e)]] += distmx[src(e), dst(e)] + else + c_vols[1, c[dst(e)]] += distmx[src(e), dst(e)] + end + end + + for _ in 1:max_moves + last_change = nothing + for v in vertex_order + if v == last_change # stop if we see each vertex and no movement + return nothing + end + potential_coms = unique(c[u] for u in all_neighbors(g, v)) + filter!(!=(c[v]), potential_coms) + @debug "Moving vertex $(v) from com $(c[v]) to potential_coms $(potential_coms)" + if isempty(potential_coms) # Continue if there are no other neighboring coms + continue + end + shuffle!(rng, potential_coms) # Break ties randomly by first com + + #Remove vertex degrees from current community + out_degree = sum( + u == v ? 2distmx[v, u] : distmx[v, u] for u in outneighbors(g, v) + ) + c_vols[1, c[v]] -= out_degree + + in_degree = 0.0 # defined outside to keep JET.jl happy + if is_directed(g) + in_degree = sum( + u == v ? 2distmx[v, u] : distmx[v, u] for u in inneighbors(g, v) + ) + c_vols[2, c[v]] -= in_degree + end + + # Compute loss in modularity by removing vertex + loss = ΔQ(g, γ, distmx, c, v, m, c[v], c_vols) + @debug "Q loss of removing vertex $(v) from its community: $(loss)" + # Compute gain by moving to alternate neighboring community + this_ΔQ = c_potential -> ΔQ(g, γ, distmx, c, v, m, c_potential, c_vols) + best_ΔQ, best_com_id = findmax(this_ΔQ, potential_coms) + best_com = potential_coms[best_com_id] + @debug "Best move is to $(best_com) with Q gain of $(best_ΔQ)" + if best_ΔQ - loss > move_tol + c[v] = best_com + c_vols[1, best_com] += out_degree + if is_directed(g) + c_vols[2, best_com] += in_degree + end + last_change = v + @debug "Moved vertex $(v) to community $(best_com)" + else + c_vols[1, c[v]] += out_degree + if is_directed(g) + c_vols[2, c[v]] += in_degree + end + @debug "Insufficient Q gain, vertex $(v) stays in community $(c[v])" + end + end + if isnothing(last_change) # No movement + return nothing + end + end +end + +""" + ΔQ(g, γ, distmx, c, v, m, c_potential, c_vols) + +Compute the change in modularity when adding vertex v a potential community. +""" +function ΔQ(g, γ, distmx, c, v, m, c_potential, c_vols) + if is_directed(g) + out_degree = 0 + com_out_degree = 0 + for u in outneighbors(g, v) + out_degree += distmx[v, u] + if c[u] == c_potential || u == v + com_out_degree += distmx[v, u] + end + end + + in_degree = 0 + com_in_degree = 0 + for u in inneighbors(g, v) + in_degree += distmx[u, v] + if c[u] == c_potential || u == v + com_in_degree += distmx[u, v] + end + end + + # Singleton special case + if c_vols[1, c_potential] == 0 && c_vols[2, c_potential] == 0 + return (com_in_degree+com_out_degree)/m - γ*2(in_degree + out_degree)/m^2 + end + return (com_in_degree+com_out_degree)/m - + γ*(in_degree*c_vols[1, c_potential]+out_degree*c_vols[2, c_potential])/m^2 + else + degree = 0 + com_degree = 0 + for u in neighbors(g, v) + degree += u == v ? 2distmx[u, v] : distmx[u, v] + if u == v + com_degree += 2distmx[u, v] + elseif c[u] == c_potential + com_degree += distmx[u, v] + end + end + # Singleton special case + if c_vols[1, c_potential] == 0 + return com_degree/2m - γ*(degree/2m)^2 + end + return com_degree/2m - γ*degree*c_vols[1, c_potential]/2m^2 + end +end + +""" + louvain_merge(g, c, distmx) + +Merge stage of the louvain algorithm. +""" +function louvain_merge(g::AbstractGraph{T}, c, distmx) where {T} + # c is assumed to be 1:nc + nc = maximum(c) + new_distmx = Dict{Tuple{T,T},eltype(distmx)}() + new_graph = is_directed(g) ? SimpleDiGraph{T}(nc) : SimpleGraph{T}(nc) + for e in edges(g) + new_src = c[src(e)] + new_dst = c[dst(e)] + if haskey(new_distmx, (new_src, new_dst)) + new_distmx[(new_src, new_dst)] += distmx[src(e), dst(e)] + else + new_distmx[(new_src, new_dst)] = distmx[src(e), dst(e)] + end + add_edge!(new_graph, new_src, new_dst) + end + + # Convert new_distmx Dict to SparseArray + r = [k[1] for k in keys(new_distmx)] + c = [k[2] for k in keys(new_distmx)] + v = [v for v in values(new_distmx)] + new_distmx = sparse(r, c, v, nc, nc) + + if !is_directed(new_graph) + new_distmx = new_distmx + transpose(new_distmx) + new_distmx[diagind(new_distmx)] ./= 2 + end + + return new_graph, new_distmx +end diff --git a/src/community/modularity.jl b/src/community/modularity.jl index e6ef28818..f673327aa 100644 --- a/src/community/modularity.jl +++ b/src/community/modularity.jl @@ -48,7 +48,7 @@ julia> barbell = blockdiag(complete_graph(3), complete_graph(3)); julia> add_edge!(barbell, 1, 4); julia> modularity(barbell, [1, 1, 1, 2, 2, 2]) -0.35714285714285715 +0.3571428571428571 julia> modularity(barbell, [1, 1, 1, 2, 2, 2], γ=0.5) 0.6071428571428571 @@ -90,14 +90,20 @@ function modularity( c2 = c[v] if c1 == c2 Q += distmx[u, v] + if u == v && !is_directed(g) + #Since we do not look at each end in outer loop + Q += distmx[u, v] + end end kout[c1] += distmx[u, v] kin[c2] += distmx[u, v] + if u == v && !is_directed(g) + #Since we do not look at each end in outer loop + kout[c1] += distmx[u, v] + kin[c2] += distmx[u, v] + end end end - Q = Q * m - @inbounds for i in 1:nc - Q -= γ * kin[i] * kout[i] - end - return Q / m^2 + Q = Q/m - γ * sum(kin .* kout) / m^2 + return Q end diff --git a/src/connectivity.jl b/src/connectivity.jl index 2aae20364..fa04231a1 100644 --- a/src/connectivity.jl +++ b/src/connectivity.jl @@ -1,26 +1,37 @@ # Parts of this code were taken / derived from Graphs.jl. See LICENSE for # licensing details. """ - connected_components!(label, g) + connected_components!(label, g, [search_queue]) Fill `label` with the `id` of the connected component in the undirected graph `g` to which it belongs. Return a vector representing the component assigned to each vertex. The component value is the smallest vertex ID in the component. -### Performance +## Optional arguments +- `search_queue`, an empty `Vector{eltype(edgetype(g))}`, can be provided to avoid + reallocating this work array repeatedly on repeated calls of `connected_components!`. + If not provided, it is automatically instantiated. + +!!! warning "Experimental" + The `search_queue` argument is experimental and subject to potential change + in future versions of Graphs.jl. + +## Performance This algorithm is linear in the number of edges of the graph. """ -function connected_components!(label::AbstractVector, g::AbstractGraph{T}) where {T} +function connected_components!( + label::AbstractVector{T}, g::AbstractGraph{T}, search_queue::Vector{T}=Vector{T}() +) where {T} + empty!(search_queue) for u in vertices(g) label[u] != zero(T) && continue label[u] = u - Q = Vector{T}() - push!(Q, u) - while !isempty(Q) - src = popfirst!(Q) + push!(search_queue, u) + while !isempty(search_queue) + src = popfirst!(search_queue) for vertex in all_neighbors(g, src) if label[vertex] == zero(T) - push!(Q, vertex) + push!(search_queue, vertex) label[vertex] = u end end @@ -129,9 +140,78 @@ julia> is_connected(g) true ``` """ -function is_connected(g::AbstractGraph) +function is_connected(g::AbstractGraph{T}) where {T} mult = is_directed(g) ? 2 : 1 - return mult * ne(g) + 1 >= nv(g) && length(connected_components(g)) == 1 + if mult * ne(g) + 1 >= nv(g) + label = zeros(T, nv(g)) + connected_components!(label, g) + return allequal(label) + else + return false + end +end + +""" + count_connected_components( g, [label, search_queue]; reset_label::Bool=false) + +Return the number of connected components in `g`. + +Equivalent to `length(connected_components(g))` but uses fewer allocations by not +materializing the component vectors explicitly. + +## Optional arguments +Mutated work arrays, `label` and `search_queue` can be provided to avoid allocating these +arrays repeatedly on repeated calls of `count_connected_components`. +For `g :: AbstractGraph{T}`, `label` must be a zero-initialized `Vector{T}` of length +`nv(g)` and `search_queue` a `Vector{T}`. See also [`connected_components!`](@ref). + +!!! warning "Experimental" + The `search_queue` and `label` arguments are experimental and subject to potential + change in future versions of Graphs.jl. + +## Keyword arguments +- `reset_label :: Bool` (default, `false`): if `true`, `label` is reset to a zero-vector + before returning. + +## Example +``` +julia> using Graphs + +julia> g = Graph(Edge.([1=>2, 2=>3, 3=>1, 4=>5, 5=>6, 6=>4, 7=>8])); + +length> connected_components(g) +3-element Vector{Vector{Int64}}: + [1, 2, 3] + [4, 5, 6] + [7, 8] + +julia> count_connected_components(g) +3 +``` +""" +function count_connected_components( + g::AbstractGraph{T}, + label::AbstractVector{T}=zeros(T, nv(g)), + search_queue::Vector{T}=Vector{T}(); + reset_label::Bool=false, +) where {T} + connected_components!(label, g, search_queue) + c = count_unique(label) + reset_label && fill!(label, zero(eltype(label))) + return c +end + +function count_unique(label::Vector{T}) where {T} + # effectively does `length(Set(label))` but faster, since `Set(label)` sizehints + # aggressively and assumes that most elements of `label` will be unique, which very + # rarely will be the case for caller `count_connected_components!` + seen = T === Int ? BitSet() : Set{T}() # if `T=Int`, we can use faster BitSet + for l in label + # faster than direct `push!(seen, l)` when `label` has few unique elements relative + # to `length(label)` + l ∉ seen && push!(seen, l) + end + return length(seen) end """ diff --git a/src/core.jl b/src/core.jl index cc9c1aa75..81c0ffb48 100644 --- a/src/core.jl +++ b/src/core.jl @@ -248,12 +248,13 @@ For directed graphs, the default is equivalent to [`outneighbors`](@ref); use [`all_neighbors`](@ref) to list inbound and outbound neighbors. ### Implementation Notes -Returns a reference to the current graph's internal structures, not a copy. -Do not modify result. If the graph is modified, the behavior is undefined: +In some cases might return a reference to the current graph's internal structures, +not a copy. Do not modify result. If the graph is modified, the behavior is undefined: the array behind this reference may be modified too, but this is not guaranteed. +If you need to modify the result use `collect` or `copy` to create a copy. # Examples -```jldoctest +```jldoctest; filter = r"0-element Graphs\\.FrozenVector\\{Int64\\}|Int64\\[\\]" julia> using Graphs julia> g = DiGraph(3); @@ -263,14 +264,14 @@ julia> add_edge!(g, 2, 3); julia> add_edge!(g, 3, 1); julia> neighbors(g, 1) -Int64[] +0-element Graphs.FrozenVector{Int64} julia> neighbors(g, 2) -1-element Vector{Int64}: +1-element Graphs.FrozenVector{Int64}: 3 julia> neighbors(g, 3) -1-element Vector{Int64}: +1-element Graphs.FrozenVector{Int64}: 1 ``` """ @@ -284,9 +285,10 @@ For undirected graphs, this is equivalent to both [`outneighbors`](@ref) and [`inneighbors`](@ref). ### Implementation Notes -Returns a reference to the current graph's internal structures, not a copy. -Do not modify result. If the graph is modified, the behavior is undefined: +In some cases might return a reference to the current graph's internal structures, +not a copy. Do not modify result. If the graph is modified, the behavior is undefined: the array behind this reference may be modified too, but this is not guaranteed. +If you need to modify the result use `collect` or `copy` to create a copy. # Examples ```jldoctest diff --git a/src/cycles/basis.jl b/src/cycles/basis.jl index 9ff04cd5e..699aff0fb 100644 --- a/src/cycles/basis.jl +++ b/src/cycles/basis.jl @@ -13,7 +13,7 @@ useful, e.g. when deriving equations for electric circuits using Kirchhoff's Laws. # Examples -```jldoctest +```jldoctest; filter = r"^\\s+\\[[\\d, ]+\\]\$"m julia> using Graphs julia> elist = [(1,2),(2,3),(2,4),(3,4),(4,1),(1,5)]; diff --git a/src/distance.jl b/src/distance.jl index df2a0d3fe..dd48270b6 100644 --- a/src/distance.jl +++ b/src/distance.jl @@ -95,6 +95,9 @@ end Given a graph and optional distance matrix, or a vector of precomputed eccentricities, return the maximum eccentricity of the graph. +An optimizied BFS algorithm (iFUB) is used for unweighted graphs, both in [undirected](https://www.sciencedirect.com/science/article/pii/S0304397512008687) +and [directed](https://link.springer.com/chapter/10.1007/978-3-642-30850-5_10) cases. + # Examples ```jldoctest julia> using Graphs @@ -106,11 +109,127 @@ julia> diameter(path_graph(5)) 4 ``` """ +function diameter end + diameter(eccentricities::Vector) = maximum(eccentricities) -function diameter(g::AbstractGraph, distmx::AbstractMatrix=weights(g)) + +diameter(g::AbstractGraph) = diameter(g, weights(g)) + +function diameter(g::AbstractGraph, ::DefaultDistance) + if nv(g) == 0 + return 0 + end + + connected = is_directed(g) ? is_strongly_connected(g) : is_connected(g) + + if !connected + return typemax(Int) + end + + return _diameter_ifub(g) +end + +function diameter(g::AbstractGraph, distmx::AbstractMatrix) return maximum(eccentricity(g, distmx)) end +function _diameter_ifub(g::AbstractGraph{T}) where {T<:Integer} + nvg = nv(g) + out_list = [outneighbors(g, v) for v in vertices(g)] + + if is_directed(g) + in_list = [inneighbors(g, v) for v in vertices(g)] + else + in_list = out_list + end + + active = trues(nvg) + visited = falses(nvg) + queue = Vector{T}(undef, nvg) + distbuf = fill(typemax(T), nvg) + diam = 0 + + # Sort vertices by total degree (descending) to maximize pruning potential + vs = collect(vertices(g)) + sort!(vs; by=v -> -(length(out_list[v]) + length(in_list[v]))) + + for u in vs + if !active[u] + continue + end + + # Forward BFS from u + fill!(visited, false) + visited[u] = true + queue[1] = u + front = 1 + back = 2 + level_end = 1 + e = 0 + + while front < back + v = queue[front] + front += 1 + + @inbounds for w in out_list[v] + if !visited[w] + visited[w] = true + queue[back] = w + back += 1 + end + end + + if front > level_end && front < back + e += 1 + level_end = back - 1 + end + end + diam = max(diam, e) + + # Backward BFS (Pruning) + dmax = diam - e + + # Only prune if we have a chance to exceed the current diameter + if dmax >= 0 + fill!(distbuf, typemax(T)) + distbuf[u] = 0 + queue[1] = u + front = 1 + back = 2 + + while front < back + v = queue[front] + front += 1 + + if distbuf[v] >= dmax + continue + end + + @inbounds for w in in_list[v] + if distbuf[w] == typemax(T) + distbuf[w] = distbuf[v] + 1 + queue[back] = w + back += 1 + end + end + end + + # Prune vertices that cannot lead to a longer diameter + @inbounds for v in vertices(g) + if active[v] && distbuf[v] != typemax(T) && (distbuf[v] + e <= diam) + active[v] = false + end + end + end + + if !any(active) + break + end + end + + return diam +end + """ periphery(eccentricities) periphery(g, distmx=weights(g)) diff --git a/src/dominatingset/degree_dom_set.jl b/src/dominatingset/degree_dom_set.jl index 3e3da690b..183355775 100644 --- a/src/dominatingset/degree_dom_set.jl +++ b/src/dominatingset/degree_dom_set.jl @@ -52,8 +52,8 @@ function dominating_set(g::AbstractGraph{T}, alg::DegreeDominatingSet) where {T< degree_queue = PriorityQueue(Base.Order.Reverse, enumerate(degree(g) .+ 1)) length_ds = 0 - while !isempty(degree_queue) && peek(degree_queue)[2] > 0 - v = dequeue!(degree_queue) + while !isempty(degree_queue) && first(degree_queue)[2] > 0 + v = popfirst!(degree_queue).first in_dom_set[v] = true length_ds += 1 diff --git a/src/editdist.jl b/src/editdist.jl index 895ff7a3e..33eaccda8 100644 --- a/src/editdist.jl +++ b/src/editdist.jl @@ -149,16 +149,16 @@ function _edit_distance( # initialize open set OPEN = PriorityQueue{Vector{Tuple},Float64}() for v in vertices(G₂) - enqueue!(OPEN, [(T(1), v)], vertex_subst_cost(1, v) + h([(T(1), v)])) + push!(OPEN, [(T(1), v)] => vertex_subst_cost(1, v) + h([(T(1), v)])) end - enqueue!(OPEN, [(T(1), U(0))], vertex_delete_cost(1) + h([(T(1), U(0))])) + push!(OPEN, [(T(1), U(0))] => vertex_delete_cost(1) + h([(T(1), U(0))])) c = 0 while true # minimum (partial) edit path - λ, cost = peek(OPEN) + λ, cost = first(OPEN) c += 1 - dequeue!(OPEN) + popfirst!(OPEN) if is_complete_path(λ, G₁, G₂) return cost, λ @@ -177,7 +177,7 @@ function _edit_distance( end new_cost += association_cost(u1, u1, v1, v1) # handle self-loops - enqueue!(OPEN, λ⁺, new_cost) + push!(OPEN, λ⁺ => new_cost) end # we try deleting v1 λ⁺ = [λ; (u1, U(0))] @@ -194,7 +194,7 @@ function _edit_distance( new_cost += edge_delete_cost(Edge(u2, u1)) end end - enqueue!(OPEN, λ⁺, new_cost) + push!(OPEN, λ⁺ => new_cost) else # add remaining vertices of G₂ to the path by deleting them λ⁺ = [λ; [(T(0), v) for v in vs]] @@ -212,7 +212,7 @@ function _edit_distance( end end end - enqueue!(OPEN, λ⁺, new_cost + h(λ⁺) - h(λ)) + push!(OPEN, λ⁺ => new_cost + h(λ⁺) - h(λ)) end end end diff --git a/src/frozenvector.jl b/src/frozenvector.jl new file mode 100644 index 000000000..c234df0c3 --- /dev/null +++ b/src/frozenvector.jl @@ -0,0 +1,25 @@ +""" + FrozenVector(v::Vector) <: AbstractVector + +A data structure that wraps a `Vector` but does not allow modifications. +""" +struct FrozenVector{T} <: AbstractVector{T} + wrapped::Vector{T} +end + +Base.size(v::FrozenVector) = Base.size(v.wrapped) + +Base.@propagate_inbounds Base.getindex(v::FrozenVector, i::Int) = Base.getindex( + v.wrapped, i +) + +Base.IndexStyle(v::Type{FrozenVector{T}}) where {T} = Base.IndexStyle(Vector{T}) + +Base.iterate(v::FrozenVector) = Base.iterate(v.wrapped) +Base.iterate(v::FrozenVector, state) = Base.iterate(v.wrapped, state) + +Base.similar(v::FrozenVector) = Base.similar(v.wrapped) +Base.similar(v::FrozenVector, T::Type) = Base.similar(v.wrapped, T) +Base.similar(v::FrozenVector, T::Type, dims::Base.Dims) = Base.similar(v.wrapped, T, dims) + +Base.copy(v::FrozenVector) = Base.copy(v.wrapped) diff --git a/src/graphcut/karger_min_cut.jl b/src/graphcut/karger_min_cut.jl index 1fbd5af7d..1d412c771 100644 --- a/src/graphcut/karger_min_cut.jl +++ b/src/graphcut/karger_min_cut.jl @@ -23,7 +23,7 @@ function karger_min_cut(g::AbstractGraph{T}) where {T<:Integer} nvg < 2 && return zeros(Int, nvg) nvg == 2 && return [1, 2] - connected_vs = IntDisjointSets(nvg) + connected_vs = IntDisjointSet(nvg) num_components = nvg for e in shuffle(collect(edges(g))) diff --git a/src/independentset/degree_ind_set.jl b/src/independentset/degree_ind_set.jl index 7ca403cf0..e465a2114 100644 --- a/src/independentset/degree_ind_set.jl +++ b/src/independentset/degree_ind_set.jl @@ -17,6 +17,9 @@ adjacent to the fewest valid vertices in the independent set until all vertices ### Performance Runtime: O((|V|+|E|)*log(|V|)) Memory: O(|V|) + +### See also +[`maximum_independent_set`](@ref) """ function independent_set(g::AbstractGraph{T}, alg::DegreeIndependentSet) where {T<:Integer} nvg = nv(g) @@ -26,7 +29,7 @@ function independent_set(g::AbstractGraph{T}, alg::DegreeIndependentSet) where { degree_queue = PriorityQueue(enumerate(degree(g))) while !isempty(degree_queue) - v = dequeue!(degree_queue) + v = popfirst!(degree_queue).first (deleted[v] || has_edge(g, v, v)) && continue deleted[v] = true push!(ind_set, v) diff --git a/src/independentset/maximal_ind_set.jl b/src/independentset/maximal_ind_set.jl index 410f9629d..b132d35a2 100644 --- a/src/independentset/maximal_ind_set.jl +++ b/src/independentset/maximal_ind_set.jl @@ -20,6 +20,9 @@ Approximation Factor: maximum(degree(g))+1 ### Optional Arguments - `rng=nothing`: set the Random Number Generator. - If `seed >= 0`, a random generator is seeded with this value. + +### See also +[`maximum_independent_set`](@ref) """ function independent_set( g::AbstractGraph{T}, diff --git a/src/interface.jl b/src/interface.jl index 4ff21a0a6..2a9c15fb0 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -281,9 +281,10 @@ has_edge(g, e) = has_edge(g, src(e), dst(e)) Return a list of all neighbors connected to vertex `v` by an incoming edge. ### Implementation Notes -Returns a reference to the current graph's internal structures, not a copy. -Do not modify result. If the graph is modified, the behavior is undefined: +In some cases might return a reference to the current graph's internal structures, +not a copy. Do not modify result. If the graph is modified, the behavior is undefined: the array behind this reference may be modified too, but this is not guaranteed. +If you need to modify the result use `collect` or `copy` to create a copy. # Examples ```jldoctest @@ -292,7 +293,7 @@ julia> using Graphs julia> g = SimpleDiGraph([0 1 0 0 0; 0 0 1 0 0; 1 0 0 1 0; 0 0 0 0 1; 0 0 0 1 0]); julia> inneighbors(g, 4) -2-element Vector{Int64}: +2-element Graphs.FrozenVector{Int64}: 3 5 ``` @@ -305,9 +306,10 @@ inneighbors(x, v) = _NI("inneighbors") Return a list of all neighbors connected to vertex `v` by an outgoing edge. # Implementation Notes -Returns a reference to the current graph's internal structures, not a copy. -Do not modify result. If the graph is modified, the behavior is undefined: +In some cases might return a reference to the current graph's internal structures, +not a copy. Do not modify result. If the graph is modified, the behavior is undefined: the array behind this reference may be modified too, but this is not guaranteed. +If you need to modify the result use `collect` or `copy` to create a copy. # Examples ```jldoctest @@ -316,7 +318,7 @@ julia> using Graphs julia> g = SimpleDiGraph([0 1 0 0 0; 0 0 1 0 0; 1 0 0 1 0; 0 0 0 0 1; 0 0 0 1 0]); julia> outneighbors(g, 4) -1-element Vector{Int64}: +1-element Graphs.FrozenVector{Int64}: 5 ``` """ diff --git a/src/linalg/nonbacktracking.jl b/src/linalg/nonbacktracking.jl index ddbae17dd..208e92bf5 100644 --- a/src/linalg/nonbacktracking.jl +++ b/src/linalg/nonbacktracking.jl @@ -118,16 +118,15 @@ function mul!(C, nbt::Nonbacktracking, B) end function coo_sparse(nbt::Nonbacktracking) - m = nbt.m - #= I,J = zeros(Int, m), zeros(Int, m) =# + m = nbt.m#= I,J = zeros(Int, m), zeros(Int, m) =# + I, J = zeros(Int, 0), zeros(Int, 0) for (e, u) in nbt.edgeidmap i, j = src(e), dst(e) for k in inneighbors(nbt.g, i) k == j && continue - v = nbt.edgeidmap[Edge(k, i)] - #= J[u] = v =# - #= I[u] = u =# + v = nbt.edgeidmap[Edge(k, i)]#= J[u] = v =##= I[u] = u =# + push!(I, v) push!(J, u) end diff --git a/src/linalg/spectral.jl b/src/linalg/spectral.jl index 756a37dde..f80e2afbe 100644 --- a/src/linalg/spectral.jl +++ b/src/linalg/spectral.jl @@ -180,7 +180,7 @@ function spectral_distance end # can't use Traitor syntax here (https://github.com/mauro3/SimpleTraits.jl/issues/36) @traitfn function spectral_distance( G₁::G, G₂::G, k::Integer -) where {G <: AbstractGraph; !IsDirected{G}} +) where {G<:AbstractGraph;!IsDirected{G}} A₁ = adjacency_matrix(G₁) A₂ = adjacency_matrix(G₂) @@ -199,7 +199,7 @@ function spectral_distance end end # can't use Traitor syntax here (https://github.com/mauro3/SimpleTraits.jl/issues/36) -@traitfn function spectral_distance(G₁::G, G₂::G) where {G <: AbstractGraph; !IsDirected{G}} +@traitfn function spectral_distance(G₁::G, G₂::G) where {G<:AbstractGraph;!IsDirected{G}} nv(G₁) == nv(G₂) || throw(ArgumentError("Spectral distance not defined for |G₁| != |G₂|")) return spectral_distance(G₁, G₂, nv(G₁)) diff --git a/src/operators.jl b/src/operators.jl index b07dda91a..b0b642e2d 100644 --- a/src/operators.jl +++ b/src/operators.jl @@ -504,11 +504,11 @@ sparse(g::AbstractGraph) = adjacency_matrix(g) length(g::AbstractGraph) = widen(nv(g)) * widen(nv(g)) ndims(g::AbstractGraph) = 2 -@traitfn function issymmetric(g::AG) where {AG <: AbstractGraph; !IsDirected{AG}} +@traitfn function issymmetric(g::AG) where {AG<:AbstractGraph;!IsDirected{AG}} return true end -@traitfn function issymmetric(g::AG) where {AG <: AbstractGraph; IsDirected{AG}} +@traitfn function issymmetric(g::AG) where {AG<:AbstractGraph;IsDirected{AG}} for e in edges(g) if !has_edge(g, reverse(e)) return false @@ -523,6 +523,9 @@ end Return the [cartesian product](https://en.wikipedia.org/wiki/Cartesian_product_of_graphs) of `g` and `h`. +The cartesian product has edges (g₁, h₁) ∼ (g₂, h₂) when +(g₁ = g₂ ∧ h₁ ∼ h₂) ∨ (g₁ ∼ g₂ ∧ h₁ = h₂). + ### Implementation Notes Preserves the eltype of the input graph. Will error if the number of vertices in the generated graph exceeds the eltype. @@ -575,6 +578,8 @@ end Return the [tensor product](https://en.wikipedia.org/wiki/Tensor_product_of_graphs) of `g` and `h`. +The tensor product has edges (g₁, h₁) ∼ (g₂, h₂) when g₁ ∼ g₂ ∧ h₁ ∼ h₂. + ### Implementation Notes Preserves the eltype of the input graph. Will error if the number of vertices in the generated graph exceeds the eltype. @@ -615,6 +620,227 @@ function tensor_product(g::G, h::G) where {G<:AbstractGraph} return z end +""" + strong_product(g, h) + +Return the [strong product](https://en.wikipedia.org/wiki/Strong_product_of_graphs) +of `g` and `h`. + +The strong product has edges (g₁, h₁) ∼ (g₂, h₂) when +(g₁ = g₂ ∧ h₁ ∼ h₂) ∨ (g₁ ∼ g₂ ∧ h₁ = h₂) ∨ (g₁ ∼ g₂ ∧ h₁ ∼ h₂). + +### Implementation Notes +Preserves the eltype of the input graph. Will error if the number of vertices +in the generated graph exceeds the eltype. + +# Examples +```jldoctest +julia> using Graphs + +julia> a = star_graph(3); + +julia> b = path_graph(3); + +julia> g = strong_product(a, b) +{9, 20} undirected simple Int64 graph + +julia> g == union(cartesian_product(a, b), tensor_product(a, b)) +true +``` +""" +function strong_product(g::G, h::G) where {G<:AbstractSimpleGraph} + z = G(nv(g) * nv(h)) + id(i, j) = (i - 1) * nv(h) + j + undirected = !is_directed(g) + for e1 in edges(g) + i1, i2 = Tuple(e1) + for e2 in edges(h) + j1, j2 = Tuple(e2) + add_edge!(z, id(i1, j1), id(i2, j2)) + if undirected + add_edge!(z, id(i1, j2), id(i2, j1)) + end + end + end + for e in edges(g) + i1, i2 = Tuple(e) + for j in vertices(h) + add_edge!(z, id(i1, j), id(i2, j)) + end + end + for e in edges(h) + j1, j2 = Tuple(e) + for i in vertices(g) + add_edge!(z, id(i, j1), id(i, j2)) + end + end + return z +end + +""" + disjunctive_product(g, h) + +Return the [disjunctive product](https://en.wikipedia.org/wiki/Graph_product) +of `g` and `h`. + +The disjunctive product has edges (g₁, h₁) ∼ (g₂, h₂) when g₁ ∼ g₂ ∨ h₁ ∼ h₂. + +### Implementation Notes +Preserves the eltype of the input graph. Will error if the number of vertices +in the generated graph exceeds the eltype. + +# Examples +```jldoctest +julia> using Graphs + +julia> a = star_graph(3); + +julia> b = path_graph(3); + +julia> g = disjunctive_product(a, b) +{9, 28} undirected simple Int64 graph + +julia> complement(g) == strong_product(complement(a), complement(b)) +true +``` +""" +function disjunctive_product(g::G, h::G) where {G<:AbstractSimpleGraph} + z = G(nv(g) * nv(h)) + id(i, j) = (i - 1) * nv(h) + j + for e in edges(g) + i1, i2 = Tuple(e) + for j in vertices(h) + for k in vertices(h) + add_edge!(z, id(i1, j), id(i2, k)) + end + end + end + for e in edges(h) + j1, j2 = Tuple(e) + for i in vertices(g) + for k in vertices(g) + add_edge!(z, id(i, j1), id(k, j2)) + end + end + end + return z +end + +""" + lexicographic_product(g, h) + +Return the [lexicographic product](https://en.wikipedia.org/wiki/Lexicographic_product_of_graphs) +of `g` and `h`. + +The lexicographic product has edges (g₁, h₁) ∼ (g₂, h₂) when (g₁ ∼ g₂) ∨ (g₁ = g₂ ∧ h₁ ∼ h₂). + +### Implementation Notes +Preserves the eltype of the input graph. Will error if the number of vertices +in the generated graph exceeds the eltype. + +# Examples +```jldoctest +julia> using Graphs + +julia> g = lexicographic_product(star_graph(3), path_graph(3)) +{9, 24} undirected simple Int64 graph + +julia> adjacency_matrix(g) +9×9 SparseArrays.SparseMatrixCSC{Int64, Int64} with 48 stored entries: + ⋅ 1 ⋅ 1 1 1 1 1 1 + 1 ⋅ 1 1 1 1 1 1 1 + ⋅ 1 ⋅ 1 1 1 1 1 1 + 1 1 1 ⋅ 1 ⋅ ⋅ ⋅ ⋅ + 1 1 1 1 ⋅ 1 ⋅ ⋅ ⋅ + 1 1 1 ⋅ 1 ⋅ ⋅ ⋅ ⋅ + 1 1 1 ⋅ ⋅ ⋅ ⋅ 1 ⋅ + 1 1 1 ⋅ ⋅ ⋅ 1 ⋅ 1 + 1 1 1 ⋅ ⋅ ⋅ ⋅ 1 ⋅ +``` +""" +function lexicographic_product(g::G, h::G) where {G<:AbstractSimpleGraph} + z = G(nv(g) * nv(h)) + id(i, j) = (i - 1) * nv(h) + j + for e in edges(g) + i1, i2 = Tuple(e) + for j in vertices(h) + for k in vertices(h) + add_edge!(z, id(i1, j), id(i2, k)) + end + end + end + for e in edges(h) + j1, j2 = Tuple(e) + for i in vertices(g) + add_edge!(z, id(i, j1), id(i, j2)) + end + end + return z +end + +""" + homomorphic_product(g, h) + +Return the [homomorphic product](https://en.wikipedia.org/wiki/Graph_product) +of `g` and `h`. + +The homomorphic product has edges (g₁, h₁) ∼ (g₂, h₂) when +(g₁ = g₂) ∨ (g₁ ∼ g₂ ∧ h₁ ≁ h₂). + +### Implementation Notes +Preserves the eltype of the input graph. Will error if the number of vertices +in the generated graph exceeds the eltype. + +# Examples +```jldoctest +julia> using Graphs + +julia> g = homomorphic_product(star_graph(3), path_graph(3)) +{9, 19} undirected simple Int64 graph + +julia> adjacency_matrix(g) +9×9 SparseArrays.SparseMatrixCSC{Int64, Int64} with 38 stored entries: + ⋅ 1 1 1 ⋅ 1 1 ⋅ 1 + 1 ⋅ 1 ⋅ 1 ⋅ ⋅ 1 ⋅ + 1 1 ⋅ 1 ⋅ 1 1 ⋅ 1 + 1 ⋅ 1 ⋅ 1 1 ⋅ ⋅ ⋅ + ⋅ 1 ⋅ 1 ⋅ 1 ⋅ ⋅ ⋅ + 1 ⋅ 1 1 1 ⋅ ⋅ ⋅ ⋅ + 1 ⋅ 1 ⋅ ⋅ ⋅ ⋅ 1 1 + ⋅ 1 ⋅ ⋅ ⋅ ⋅ 1 ⋅ 1 + 1 ⋅ 1 ⋅ ⋅ ⋅ 1 1 ⋅ +``` +""" +function homomorphic_product(g::G, h::G) where {G<:AbstractSimpleGraph} + z = G(nv(g) * nv(h)) + id(i, j) = (i - 1) * nv(h) + j + undirected = !is_directed(g) + for i in vertices(g) + for j in vertices(h) + for k in vertices(h) + if k != j + add_edge!(z, id(i, j), id(i, k)) + end + end + end + end + cmpl_h = complement(h) + for e in edges(g) + i1, i2 = Tuple(e) + for f in edges(cmpl_h) + j1, j2 = Tuple(f) + add_edge!(z, id(i1, j1), id(i2, j2)) + if undirected + add_edge!(z, id(i1, j2), id(i2, j1)) + end + end + for j in vertices(h) + add_edge!(z, id(i1, j), id(i2, j)) + end + end + return z +end + ## subgraphs ### """ @@ -881,86 +1107,148 @@ function merge_vertices!(g::Graph{T}, vs::Vector{U} where {U<:Integer}) where {T end """ - line_graph(g::SimpleGraph) ::SimpleGraph + line_graph(g::SimpleGraph; loops::Symbol=:none) ::SimpleGraph + Given a graph `g`, return the graph `lg`, whose vertices are integers that enumerate the edges in `g`, and two vertices in `lg` form an edge iff the corresponding edges in `g` share a common endpoint. In other words, edges in `lg` are length-2 paths in `g`. -Note that `i ∈ vertices(lg)` corresponds to `collect(edges(g))[i]`. +Note that `k ∈ vertices(lg)` corresponds to `collect(edges(g))[k]`. + +The argument `loops` determines how self-loops in `lg` are handled: +- `loops = :none` means that `lg` will contain no loops `k-k`; +- `loops = :inherit` means that only a loop `v-v = edges(g)[k]` induces a loop `k-k` in `lg`; +- `loops = :all` means that every `edges(g)[k]` induces a loop `k-k` in `lg`. # Examples ```jldoctest julia> using Graphs -julia> g = path_graph(5); +julia> g = path_graph(5); # path with 4 edges -julia> lg = line_graph(g) +julia> lg = line_graph(g) # path with 3 edges {4, 3} undirected simple Int64 graph + +julia> g = cycle_graph(4); # cycle with 4 edges + +julia> lg = line_graph(g) # cycle with 4 edges +{4, 4} undirected simple Int64 graph + +julia> g = star_graph(6); # star with 5 edges + +julia> lg = line_graph(g) # complete graph with 10 edges +{5, 10} undirected simple Int64 graph + +julia> g = SimpleGraph(3, [[2],[1,2,3],[2]]); # vertices 1:3, edges 1-2, 2-2, 2-3 + +julia> lg = line_graph(g, loops=:none) # vertices 1:3, edges 1-2, 1-3, 2-3 +{3, 3} undirected simple Int64 graph + +julia> lg = line_graph(g, loops=:inherit) # vertices 1:3, edges 1-2, 1-3, 2-3, 2-2 +{3, 4} undirected simple Int64 graph + +julia> lg = line_graph(g, loops=:all) # vertices 1:3, edges 1-2, 1-3, 2-3, 1-1, 2-2, 3-3 +{3, 6} undirected simple Int64 graph ``` """ -function line_graph(g::SimpleGraph) +function line_graph(g::SimpleGraph; loops::Symbol=:none) + @assert loops in (:none, :inherit, :all) vertex_to_edges = [Int[] for _ in 1:nv(g)] - for (i, e) in enumerate(edges(g)) + is_loop = BitVector(undef, ne(g)) + for (k, e) in enumerate(edges(g)) s, d = src(e), dst(e) - push!(vertex_to_edges[s], i) - s == d && continue # do not push self-loops twice - push!(vertex_to_edges[d], i) + is_loop[k] = (loops == :none ? false : (loops == :all ? true : s == d)) + push!(vertex_to_edges[s], k) + s==d && continue # do not push self-loops twice + push!(vertex_to_edges[d], k) end - fadjlist = [Int[] for _ in 1:ne(g)] # edge to neighbors adjacency in lg - m = 0 # number of edges in the line-graph - for es in vertex_to_edges - n = length(es) - for i in 1:(n - 1), j in (i + 1):n # iterate through pairs of edges with same endpoint - ei, ej = es[i], es[j] + fadjlist = [Int[] for _ in 1:ne(g)] # edge to neighbor-edges adjacency in lg + m = 0 # number of edges in lg + for ee in vertex_to_edges # edges ee in g induce a clique in lg + n = length(ee) + for i in 1:n, j in i:n # iterate through pairs of edges with same endpoint + ei, ej = ee[i], ee[j] + ei==ej && !is_loop[ei] && continue + is_loop[ei] = false # prevent non-loop edge in g to be added twice as a loop in lg m += 1 push!(fadjlist[ei], ej) push!(fadjlist[ej], ei) end end - foreach(sort!, fadjlist) + for list in fadjlist + !issorted(list) && sort!(list) # O(n) check, O(n*logn) correction + end return SimpleGraph(m, fadjlist) end """ - line_graph(g::SimpleDiGraph) ::SimpleDiGraph + line_graph(g::SimpleDiGraph; loops::Symbol=:none) ::SimpleDiGraph + Given a digraph `g`, return the digraph `lg`, whose vertices are integers that enumerate the edges in `g`, and there is an edge in `lg` from `Edge(a,b)` to `Edge(c,d)` iff b==c. In other words, edges in `lg` are length-2 directed paths in `g`. -Note that `i ∈ vertices(lg)` corresponds to `collect(edges(g))[i]`. +Note that `k ∈ vertices(lg)` corresponds to `collect(edges(g))[k]`. + +The argument `loops` determines how self-loops in `lg` are handled: +- `loops = :none` means that `lg` will contain no loops `k-k`; +- `loops = :inherit` means that every loop `v->v = edges(g)[k]` induces a loop `k-k` in `lg`. # Examples ```jldoctest julia> using Graphs -julia> g = cycle_digraph(5); +julia> g = path_digraph(5); # path with 4 edges + +julia> lg = line_graph(g) # path with 3 edges +{4, 3} directed simple Int64 graph + +julia> g = cycle_digraph(4); # cycle with 4 edges + +julia> lg = line_graph(g) # cycle with 4 edges +{4, 4} directed simple Int64 graph + +julia> g = SimpleDiGraphFromIterator(Edge(k≤3 ? k+1 : 1, k≤3 ? 1 : k+1) for k in 1:3+4); # star, 3 in, 4 out -julia> lg = line_graph(g) -{5, 5} directed simple Int64 graph +julia> lg = line_graph(g) # complete bipartite digraph with 3*4 edges +{7, 12} directed simple Int64 graph + +julia> g = SimpleDiGraph(3, [[2],[2,3],Int[]], [Int[],[1,2],[2]]); # vertices 1:3, edges 1->2, 2->2, 2->3 + +julia> lg = line_graph(g, loops=:none) # vertices 1:3, edges 1->2, 1->3, 2->3 +{3, 3} directed simple Int64 graph + +julia> lg = line_graph(g, loops=:inherit) # vertices 1:3, edges 1->2, 1->3, 2->3, 2->2 +{3, 4} directed simple Int64 graph ``` """ -function line_graph(g::SimpleDiGraph) +function line_graph(g::SimpleDiGraph; loops::Symbol=:none) + @assert loops in (:none, :inherit) vertex_to_edgesout = [Int[] for _ in 1:nv(g)] vertex_to_edgesin = [Int[] for _ in 1:nv(g)] - for (i, e) in enumerate(edges(g)) + for (k, e) in enumerate(edges(g)) s, d = src(e), dst(e) - push!(vertex_to_edgesout[s], i) - push!(vertex_to_edgesin[d], i) + push!(vertex_to_edgesout[s], k) + push!(vertex_to_edgesin[d], k) end - fadjilist = [Int[] for _ in 1:ne(g)] # edge to neighbors forward adjacency in lg - badjilist = [Int[] for _ in 1:ne(g)] # edge to neighbors backward adjacency in lg + fadjlist = [Int[] for _ in 1:ne(g)] # edge to neighbors forward adjacency in lg + badjlist = [Int[] for _ in 1:ne(g)] # edge to neighbors backward adjacency in lg m = 0 # number of edges in the line-graph for (e_i, e_o) in zip(vertex_to_edgesin, vertex_to_edgesout) for ei in e_i, eo in e_o # iterate through length-2 directed paths - ei == eo && continue # a self-loop in g does not induce a self-loop in lg + ei == eo && loops == :none && continue # a self-loop in g does not induce a self-loop in lg m += 1 - push!(fadjilist[ei], eo) - push!(badjilist[eo], ei) + push!(fadjlist[ei], eo) + push!(badjlist[eo], ei) end end - foreach(sort!, fadjilist) - foreach(sort!, badjilist) - return SimpleDiGraph(m, fadjilist, badjilist) + for list in fadjlist + !issorted(list) && sort!(list) # O(n) check, O(n*logn) correction + end + for list in badjlist + !issorted(list) && sort!(list) # O(n) check, O(n*logn) correction + end + return SimpleDiGraph(m, fadjlist, badjlist) end diff --git a/src/persistence/lg.jl b/src/persistence/lg.jl index f45e70790..6c7b5d088 100644 --- a/src/persistence/lg.jl +++ b/src/persistence/lg.jl @@ -58,23 +58,23 @@ function _lg_skip_one_graph(f::IO, n_e::Integer) end function _parse_header(s::AbstractString) - addl_info = false nvstr, nestr, dirundir, graphname = split(s, r"s*,s*"; limit=4) + addl = nothing if occursin(",", graphname) # version number and type graphname, _ver, _dtype, graphcode = split(graphname, r"s*,s*") ver = parse(Int, _ver) dtype = getfield(Main, Symbol(_dtype)) - addl_info = true + addl = (ver, dtype, graphcode) end n_v = parse(Int, nvstr) n_e = parse(Int, nestr) dirundir = strip(dirundir) directed = !(dirundir == "u") graphname = strip(graphname) - if !addl_info + if addl === nothing header = LGHeader(n_v, n_e, directed, graphname) else - header = LGHeader(n_v, n_e, directed, graphname, ver, dtype, graphcode) + header = LGHeader(n_v, n_e, directed, graphname, addl...) end return header end diff --git a/src/planarity.jl b/src/planarity.jl new file mode 100644 index 000000000..2ea221bf0 --- /dev/null +++ b/src/planarity.jl @@ -0,0 +1,454 @@ +# Planarity algorithm for Julia graphs. +# Algorithm from https://www.uni-konstanz.de/algo/publications/b-lrpt-sub.pdf +# The implementation is heavily influenced by the recursive implementation in Networkx (https://networkx.org/documentation/stable/_modules/networkx/algorithms/planarity.html) + +import DataStructures: DefaultDict, Stack +import Base: isempty + +""" + is_planar(g) + +Determines whether or not the graph `g` is [planar](https://en.wikipedia.org/wiki/Planar_graph). + +Uses the [left-right planarity test](https://en.wikipedia.org/wiki/Left-right_planarity_test). + +### References +- [Brandes 2009](https://www.uni-konstanz.de/algo/publications/b-lrpt-sub.pdf) +""" +function is_planar(g) + lrp = LRPlanarity(g) + return lr_planarity!(lrp) +end + +#Simple structs to be used in algorithm. Keep private for now. +function empty_edge(T) + return Edge{T}(0, 0) +end + +function isempty(e::Edge{T}) where {T} + return src(e) == zero(T) && dst(e) == zero(T) +end + +mutable struct Interval{T} + high::Edge{T} + low::Edge{T} +end + +function empty_interval(T) + return Interval(empty_edge(T), empty_edge(T)) +end + +function isempty(interval::Interval) + return isempty(interval.high) && isempty(interval.low) +end + +function conflicting(interval::Interval{T}, b, lrp_state) where {T} + return !isempty(interval) && (lrp_state.lowpt[interval.high] > lrp_state.lowpt[b]) +end + +mutable struct ConflictPair{T} + L::Interval{T} + R::Interval{T} +end + +function empty_pair(T) + return ConflictPair(empty_interval(T), empty_interval(T)) +end + +function swap!(self::ConflictPair) + #Swap left and right intervals + temp = self.L + self.L = self.R + return self.R = temp +end + +function root_pair(T) + #returns the "root pair" of type T + e = Edge{T}(0, 0) + return ConflictPair(Interval(e, e), Interval(e, e)) +end + +function isempty(p::ConflictPair) + return isempty(p.L) && isempty(p.R) +end + +# ATM (Julia 1.8.4, DataStructures v0.18.13), DefaultDict crashes +# for large order matrices when we attempt to trim the back edges. +#To fix this we create a manual version of the DefaultDict that seems to be more stable + +struct ManualDict{A,B} + d::Dict{A,B} + default::B +end + +function ManualDict(A, B, default) + return ManualDict(Dict{A,B}(), default) +end + +import Base: getindex + +function getindex(md::ManualDict, x) + d = md.d + if haskey(d, x) + d[x] + else + d[x] = md.default + md.default + end +end + +function setindex!(md::ManualDict, X, key) + return setindex!(md.d, X, key) +end + +mutable struct LRPlanarity{T<:Integer} + #State class for the planarity test + #We index by Edge structs throughout as it is easier than switching between + #Edges and tuples + #G::SimpleGraph{T} #Copy of the input graph + V::Int64 + E::Int64 + roots::Vector{T} #Vector of roots for disconnected graphs. Normally size(roots, 1) == 1 + height::DefaultDict{T,Int64} #DefaultDict of heights <: Int, indexed by node. default is -1 + lowpt::Dict{Edge{T},Int64} #Dict of low points, indexed by Edge + lowpt2::Dict{Edge{T},Int64} #Dict of low points (copy), indexed by Edge + nesting_depth::Dict{Edge{T},Int64} #Dict of nesting depths, indexed by Edge + parent_edge::DefaultDict{T,Edge{T}} #Dict of parent edges, indexed by node + DG::SimpleDiGraph{T} #Directed graph for the orientation phase + adjs::Dict{T,Vector{T}} #Dict of neighbors of nodes, indexed by node + ordered_adjs::Dict{T,Vector{T}} #Dict of neighbors of nodes sorted by nesting depth, indexed by node + ref::DefaultDict{Edge{T},Edge{T}} #ManualDict of Edges, indexed by Edge + side::DefaultDict{Edge{T},Int8} #DefaultDict of +/- 1, indexed by edge + S::Stack{ConflictPair{T}} #Stack of tuples of Edges + stack_bottom::Dict{Edge{T},ConflictPair{T}} #Dict of Tuples of Edges, indexed by Edge + lowpt_edge::Dict{Edge{T},Edge{T}} #Dict of Edges, indexed by Edge + #left_ref::Dict{T,Edge{T}} #Dict of Edges, indexed by node + #right_ref::Dict{T,Edge{T}} #Dict of Edges, indexed by node + # skip embedding for now +end + +#outer constructor for LRPlanarity +function LRPlanarity(g::AG) where {AG<:AbstractGraph} + V = Int64(nv(g)) #needs promoting + E = Int64(ne(g)) #JIC + #record nodetype of g + T = eltype(g) + N = nv(g) + + roots = T[] + + # distance from tree root + height = DefaultDict{T,Int64}(-1) + + lowpt = Dict{Edge{T},Int64}() # height of lowest return point of an edge + lowpt2 = Dict{Edge{T},Int64}() # height of second lowest return point + nesting_depth = Dict{Edge{T},Int64}() # for nesting order + + # None == Edge(0, 0) for our type-stable algo + + parent_edge = DefaultDict{T,Edge{T}}(empty_edge(T)) + + # oriented DFS graph + DG = SimpleDiGraph{T}(N) + + adjs = Dict{T,Vector{T}}() + # make adjacency lists for dfs + for v in 1:nv(g) #for all vertices in G, + adjs[v] = all_neighbors(g, v) ##neighbourhood of v + end + + ordered_adjs = Dict{T,Vector{T}}() + + ref = DefaultDict{Edge{T},Edge{T}}(empty_edge(T)) + side = DefaultDict{Edge{T},Int8}(one(Int8)) + + # stack of conflict pairs + S = Stack{ConflictPair{T}}() + stack_bottom = Dict{Edge{T},ConflictPair{T}}() + lowpt_edge = Dict{Edge{T},Edge{T}}() + #left_ref = Dict{T,Edge{T}}() + #right_ref = Dict{T,Edge{T}}() + + #self.embedding = PlanarEmbedding() + return LRPlanarity( + #g, + V, + E, + roots, + height, + lowpt, + lowpt2, + nesting_depth, + parent_edge, + DG, + adjs, + ordered_adjs, + ref, + side, + S, + stack_bottom, + lowpt_edge, + #left_ref, + #right_ref, + ) +end + +function lrp_type(lrp::LRPlanarity{T}) where {T} + T +end + +function lowest(self::ConflictPair, planarity_state::LRPlanarity) + #Returns the lowest lowpoint of a conflict pair + if isempty(self.L) + return planarity_state.lowpt[self.R.low] + end + + if isempty(self.R) + return planarity_state.lowpt[self.L.low] + end + + return min(planarity_state.lowpt[self.L.low], planarity_state.lowpt[self.R.low]) +end + +function lr_planarity!(self::LRPlanarity{T}) where {T} + V = self.V + E = self.E + + if V > 2 && (E > (3V - 6)) + # graph is not planar + return false + end + + # orientation of the graph by depth first search traversal + for v in 1:V + if self.height[v] == -one(T) #using -1 rather than nothing for type stability. + self.height[v] = zero(T) + push!(self.roots, v) + dfs_orientation!(self, v) + end + end + + #Testing stage + #First, sort the ordered_adjs by nesting depth + for v in 1:V #for all vertices in G, + #get neighboring nodes + neighboring_nodes = T[] + neighboring_nesting_depths = Int64[] + for (k, value) in self.nesting_depth + if k.src == v + push!(neighboring_nodes, k.dst) + push!(neighboring_nesting_depths, value) + end + end + neighboring_nodes .= neighboring_nodes[sortperm(neighboring_nesting_depths)] + self.ordered_adjs[v] = neighboring_nodes + end + for s in self.roots + if !dfs_testing!(self, s) + return false + end + end + + #if the algorithm finishes, the graph is planar. Return true + return true +end + +function dfs_orientation!(self::LRPlanarity, v) + # get the parent edge of v. + # if v is a root, the parent_edge dict + # will return Edge(0, 0) + e = self.parent_edge[v] #get the parent edge of v. + #orient all edges in graph recursively + for w in self.adjs[v] + # see if vw = Edge(v, w) has already been oriented + vw = Edge(v, w) + wv = Edge(w, v) #Need to consider approach from both direction + if vw in edges(self.DG) || wv in edges(self.DG) + continue + end + + #otherwise, appended to DG + add_edge!(self.DG, vw) + + #record lowpoints + self.lowpt[vw] = self.height[v] + self.lowpt2[vw] = self.height[v] + #if height == -1, i.e. we are at a tree edge, then + # record the height accordingly + if self.height[w] == -1 ##tree edge + self.parent_edge[w] = vw + self.height[w] = self.height[v] + 1 + dfs_orientation!(self, w) + else + #at a back edge - no need to + #go through a DFS + self.lowpt[vw] = self.height[w] + end + + #determine nesting depth with formulae from Brandes + #note that this will only be carried out + #once per edge + # if the edge is chordal, use the alternative formula + self.nesting_depth[vw] = 2 * self.lowpt[vw] + if self.lowpt2[vw] < self.height[v] + #chordal + self.nesting_depth[vw] += 1 + end + + #update lowpoints of parent + if !isempty(e) #if e != root + if self.lowpt[vw] < self.lowpt[e] + self.lowpt2[e] = min(self.lowpt[e], self.lowpt2[vw]) + self.lowpt[e] = self.lowpt[vw] + elseif self.lowpt[vw] > self.lowpt[e] + self.lowpt2[e] = min(self.lowpt2[e], self.lowpt[vw]) + else + self.lowpt2[e] = min(self.lowpt2[e], self.lowpt2[vw]) + end + end + end +end + +function dfs_testing!(self, v) + T = typeof(v) + e = self.parent_edge[v] + for w in self.ordered_adjs[v] #already ordered + ei = Edge(v, w) + if !isempty(self.S) #stack is not empty + self.stack_bottom[ei] = first(self.S) + else #stack is empty + self.stack_bottom[ei] = root_pair(T) + end + + if ei == self.parent_edge[ei.dst] #tree edge + if !dfs_testing!(self, ei.dst) #half if testing fails + return false + end + else #back edge + self.lowpt_edge[ei] = ei + push!(self.S, ConflictPair(empty_interval(T), Interval(ei, ei))) + end + + #integrate new return edges + if self.lowpt[ei] < self.height[v] #ei has return edge + e1 = Edge(v, first(self.ordered_adjs[v])) + if ei == e1 + self.lowpt_edge[e] = self.lowpt_edge[ei] #in Brandes this is e <- e1. Corrected in Python source? + else + #add contraints (algo 4) + if !edge_constraints!(self, ei, e) #half if fails + return false + end + end + end + end + + #remove back edges returning to parent + if !isempty(e)#v is not root + u = src(e) + #trim edges ending at parent u, algo 5 + trim_back_edges!(self, u) + #side of e is side of highest returning edge + if self.lowpt[e] < self.height[u] #e has return edge + hl = first(self.S).L.high + hr = first(self.S).R.high + if !isempty(hl) && (isempty(hr) || (self.lowpt[hl] > self.lowpt[hr])) + self.ref[e] = hl + else + self.ref[e] = hr + end + end + end + return true +end + +function edge_constraints!(self, ei, e) + T = eltype(ei) + P = empty_pair(T) + #merge return edges of ei into P.R + while first(self.S) != self.stack_bottom[ei] + Q = pop!(self.S) + if !isempty(Q.L) + swap!(Q) + end + if !isempty(Q.L) #not planar + return false + else + if self.lowpt[Q.R.low] > self.lowpt[e] #merge intervals + if isempty(P.R) #topmost interval + P.R.high = Q.R.high + else + self.ref[P.R.low] = Q.R.high + end + P.R.low = Q.R.low + else #align + self.ref[Q.R.low] = self.lowpt_edge[e] + end + end + end + + #merge conflicting return edges of into P.LRPlanarity + while conflicting(first(self.S).L, ei, self) || conflicting(first(self.S).R, ei, self) + Q = pop!(self.S) + if conflicting(Q.R, ei, self) + swap!(Q) + end + if conflicting(Q.R, ei, self) #not planar + return false + else #merge interval below into P.R + self.ref[P.R.low] = Q.R.high + if !isempty(Q.R.low) + P.R.low = Q.R.low + end + end + if isempty(P.L) #topmost interval + P.L.high = Q.L.high + else + self.ref[P.L.low] = Q.L.high + end + P.L.low = Q.L.low + end + if !isempty(P) + push!(self.S, P) + end + return true +end + +function trim_back_edges!(self, u) + #trim back edges ending at u + #drop entire conflict pairs + while !isempty(self.S) && (lowest(first(self.S), self) == self.height[u]) + P = pop!(self.S) + if !isempty(P.L.low) + self.side[P.L.low] = -1 + end + end + + if !isempty(self.S) #one more conflict pair to consider + P = pop!(self.S) + #trim left interval + while !isempty(P.L.high) && P.L.high.dst == u + P.L.high = self.ref[P.L.high] + end + + if isempty(P.L.high) && !isempty(P.L.low) #just emptied + self.ref[P.L.low] = P.R.low + self.side[P.L.low] = -1 + T = typeof(u) + P.L.low = empty_edge(T) + end + + #trim right interval + while !isempty(P.R.high) && P.R.high.dst == u + P.R.high = self.ref[P.R.high] + end + + if isempty(P.R.high) && !isempty(P.R.low) #just emptied + self.ref[P.R.low] = P.L.low + self.side[P.R.low] = -1 + T = typeof(u) + P.R.low = empty_edge(T) + end + push!(self.S, P) + end +end diff --git a/src/shortestpaths/astar.jl b/src/shortestpaths/astar.jl index d42bd442c..aa7b553e1 100644 --- a/src/shortestpaths/astar.jl +++ b/src/shortestpaths/astar.jl @@ -31,7 +31,7 @@ function a_star_impl!( total_path = Vector{edgetype_to_return}() @inbounds while !isempty(open_set) - current = dequeue!(open_set) + current = popfirst!(open_set).first if current == goal reconstruct_path!(total_path, came_from, current, g, edgetype_to_return) @@ -86,7 +86,7 @@ function a_star( checkbounds(distmx, Base.OneTo(nv(g)), Base.OneTo(nv(g))) open_set = PriorityQueue{U,T}() - enqueue!(open_set, s, 0) + push!(open_set, s => 0) closed_set = zeros(Bool, nv(g)) diff --git a/src/shortestpaths/dijkstra.jl b/src/shortestpaths/dijkstra.jl index 9a8e8911c..cc810b276 100644 --- a/src/shortestpaths/dijkstra.jl +++ b/src/shortestpaths/dijkstra.jl @@ -95,7 +95,7 @@ function dijkstra_shortest_paths( sizehint!(closest_vertices, nvg) while !isempty(H) - u = dequeue!(H) + u = popfirst!(H).first if trackvertices push!(closest_vertices, u) diff --git a/src/shortestpaths/yen.jl b/src/shortestpaths/yen.jl index 50625044b..fbc168763 100644 --- a/src/shortestpaths/yen.jl +++ b/src/shortestpaths/yen.jl @@ -88,7 +88,7 @@ function yen_k_shortest_paths( distpath = distrootpath + djspur.dists[target] # Add the potential k-shortest path to the heap if !haskey(B, pathtotal) - enqueue!(B, pathtotal, distpath) + push!(B, pathtotal => distpath) end end @@ -99,11 +99,11 @@ function yen_k_shortest_paths( # No more paths in B isempty(B) && break - mindistB = peek(B)[2] + mindistB = first(B)[2] # The path with minimum distance in B is higher than maxdist mindistB > maxdist && break - push!(dists, peek(B)[2]) - push!(A, dequeue!(B)) + push!(dists, first(B)[2]) + push!(A, popfirst!(B).first) end return YenState{T,U}(dists, A) diff --git a/src/spanningtrees/boruvka.jl b/src/spanningtrees/boruvka.jl index 02b038a5c..26c7261c6 100644 --- a/src/spanningtrees/boruvka.jl +++ b/src/spanningtrees/boruvka.jl @@ -15,7 +15,7 @@ function boruvka_mst end @traitfn function boruvka_mst( g::AG::(!IsDirected), distmx::AbstractMatrix{T}=weights(g); minimize=true ) where {T<:Number,U,AG<:AbstractGraph{U}} - djset = IntDisjointSets(nv(g)) + djset = IntDisjointSet(nv(g)) # maximizing Z is the same as minimizing -Z # mode will indicate the need for the -1 multiplication mode = minimize ? 1 : -1 diff --git a/src/spanningtrees/kruskal.jl b/src/spanningtrees/kruskal.jl index b9efba367..3edec296e 100644 --- a/src/spanningtrees/kruskal.jl +++ b/src/spanningtrees/kruskal.jl @@ -1,9 +1,12 @@ """ kruskal_mst(g, distmx=weights(g); minimize=true) + kruskal_mst(g, weight_vector; minimize=true) Return a vector of edges representing the minimum (by default) spanning tree of a connected, undirected graph `g` with optional distance matrix `distmx` using [Kruskal's algorithm](https://en.wikipedia.org/wiki/Kruskal%27s_algorithm). +Alternative to the distance matrix `distmx`, one can pass a `weight_vector` with weights ordered as `edges(g)`. + ### Optional Arguments - `minimize=true`: if set to `false`, calculate the maximum spanning tree. """ @@ -12,20 +15,25 @@ function kruskal_mst end @traitfn function kruskal_mst( g::AG::(!IsDirected), distmx::AbstractMatrix{T}=weights(g); minimize=true ) where {T<:Number,U,AG<:AbstractGraph{U}} - connected_vs = IntDisjointSets(nv(g)) + weight_vector = Vector{T}() + sizehint!(weight_vector, ne(g)) + for e in edges(g) + push!(weight_vector, distmx[src(e), dst(e)]) + end + return kruskal_mst(g, weight_vector; minimize=minimize) +end + +@traitfn function kruskal_mst( + g::AG::(!IsDirected), weight_vector::AbstractVector{T}; minimize=true +) where {T<:Number,U,AG<:AbstractGraph{U}} + connected_vs = IntDisjointSet(nv(g)) mst = Vector{edgetype(g)}() nv(g) <= 1 && return mst sizehint!(mst, nv(g) - 1) - weights = Vector{T}() - sizehint!(weights, ne(g)) edge_list = collect(edges(g)) - for e in edge_list - push!(weights, distmx[src(e), dst(e)]) - end - - for e in edge_list[sortperm(weights; rev=!minimize)] + for e in edge_list[sortperm(weight_vector; rev=(!minimize))] if !in_same_set(connected_vs, src(e), dst(e)) union!(connected_vs, src(e), dst(e)) push!(mst, e) diff --git a/src/spanningtrees/planar_maximally_filtered_graph.jl b/src/spanningtrees/planar_maximally_filtered_graph.jl new file mode 100644 index 000000000..5e823be87 --- /dev/null +++ b/src/spanningtrees/planar_maximally_filtered_graph.jl @@ -0,0 +1,60 @@ +""" + planar_maximally_filtered_graph(g) + +Compute the Planar Maximally Filtered Graph (PMFG) of weighted graph `g`. Returns a `SimpleGraph{eltype(g)}`. + +### Examples +``` +using Graphs, SimpleWeightedGraphs +N = 20 +M = Symmetric(randn(N, N)) +g = SimpleWeightedGraph(M) +p_g = planar_maximally_filtered_graph(g) +``` + +### References +- [Tuminello et al. 2005](https://doi.org/10.1073/pnas.0500298102) +""" +function planar_maximally_filtered_graph( + g::AG, distmx::AbstractMatrix{T}=weights(g); minimize=true +) where {T<:Real,U,AG<:AbstractGraph{U}} + + #if graph has <= 6 edges, just return it + if ne(g) <= 6 + test_graph = SimpleGraph{U}(nv(g)) + for e in edges(g) + add_edge!(test_graph, e) + end + return test_graph + end + + #construct a list of edge weights + edge_list = collect(edges(g)) + weights = [distmx[src(e), dst(e)] for e in edge_list] + #sort the set of edges by weight + #we try to maximally filter the graph and assume that weights + #represent distances. Thus we want to add edges with the + #smallest distances first. We sort edges in ascending order + #of weight (if minimize=true) and iterate from the start of + #the list. + edge_list .= edge_list[sortperm(weights; rev=(!minimize))] + + #construct an initial graph + test_graph = SimpleGraph{U}(nv(g)) + + for e in edge_list[1:min(6, length(edge_list))] + #we can always add the first six edges of a graph + add_edge!(test_graph, src(e), dst(e)) + end + + #go through the rest of the edge list + for e in edge_list[7:end] + add_edge!(test_graph, src(e), dst(e)) #add it to graph + if !is_planar(test_graph) #if resulting graph is not planar, remove it again + rem_edge!(test_graph, src(e), dst(e)) + end + (ne(test_graph) >= 3 * nv(test_graph) - 6) && break #break if limit reached + end + + return test_graph +end diff --git a/src/spanningtrees/prim.jl b/src/spanningtrees/prim.jl index baf963b1c..efde13fe8 100644 --- a/src/spanningtrees/prim.jl +++ b/src/spanningtrees/prim.jl @@ -20,7 +20,7 @@ function prim_mst end wt[1] = typemin(T) while !isempty(pq) - v = dequeue!(pq) + v = popfirst!(pq).first finished[v] = true for u in neighbors(g, v) diff --git a/src/traversals/maxadjvisit.jl b/src/traversals/maxadjvisit.jl index ab8bd6b52..94827a1bc 100644 --- a/src/traversals/maxadjvisit.jl +++ b/src/traversals/maxadjvisit.jl @@ -30,7 +30,7 @@ assumed to be 1. # still appearing in fadjlist. When iterating neighbors, is_merged makes sure we # don't consider them is_merged = falses(nvg) - merged_vertices = IntDisjointSets(U(nvg)) + merged_vertices = IntDisjointSet(U(nvg)) graph_size = nvg # We need to mutate the weight matrix, # and we need it clean (0 for non edges) @@ -73,7 +73,7 @@ assumed to be 1. local cutweight while true last_vertex = u - u, cutweight = dequeue_pair!(pq) + u, cutweight = popfirst!(pq) isempty(pq) && break for v in fadjlist[u] (is_processed[v] || is_merged[v] || u == v) && continue @@ -158,7 +158,7 @@ function maximum_adjacency_visit( # start traversing the graph while !isempty(pq) - u = dequeue!(pq) + u = popfirst!(pq).first has_key[u] = false push!(vertices_order, u) log && println(io, "discover vertex: $u") diff --git a/src/vertexcover/degree_vertex_cover.jl b/src/vertexcover/degree_vertex_cover.jl index 1d9f33f34..bf850f5cc 100644 --- a/src/vertexcover/degree_vertex_cover.jl +++ b/src/vertexcover/degree_vertex_cover.jl @@ -36,8 +36,8 @@ function vertex_cover(g::AbstractGraph{T}, alg::DegreeVertexCover) where {T<:Int length_cover = 0 degree_queue = PriorityQueue(Base.Order.Reverse, enumerate(degree(g))) - while !isempty(degree_queue) && peek(degree_queue)[2] > 0 - v = dequeue!(degree_queue) + while !isempty(degree_queue) && first(degree_queue)[2] > 0 + v = popfirst!(degree_queue).first in_cover[v] = true length_cover += 1 diff --git a/src/wrappedGraphs/graphviews.jl b/src/wrappedGraphs/graphviews.jl new file mode 100644 index 000000000..aace86be1 --- /dev/null +++ b/src/wrappedGraphs/graphviews.jl @@ -0,0 +1,136 @@ +""" + ReverseView{T<:Integer,G<:AbstractGraph} <: AbstractGraph{T} + +A graph view that wraps a directed graph and reverse the direction of every edge. + +!!! warning + Some properties of the view (e.g. the number of edges) are forwarded from the + underlying graph and are not recomputed. Modifying the underlying graph after + constructing the view may lead to incorrect results. + +# Examples +```jldoctest; filter = r"0-element Graphs\\.FrozenVector\\{Int64\\}|Int64\\[\\]" +julia> using Graphs + +julia> g = SimpleDiGraph(2); + +julia> add_edge!(g, 1, 2); + +julia> rg = ReverseView(g); + +julia> neighbors(rg, 1) +0-element Graphs.FrozenVector{Int64} + +julia> neighbors(rg, 2) +1-element Graphs.FrozenVector{Int64}: + 1 +``` +""" +struct ReverseView{T<:Integer,G<:AbstractGraph} <: AbstractGraph{T} + g::G + + @traitfn ReverseView{T,G}(g::::(IsDirected)) where {T<:Integer,G<:AbstractGraph{T}} = new( + g + ) + @traitfn ReverseView{T,G}(g::::(!IsDirected)) where {T<:Integer,G<:AbstractGraph{T}} = throw( + ArgumentError("Your graph needs to be directed") + ) +end + +ReverseView(g::G) where {T<:Integer,G<:AbstractGraph{T}} = ReverseView{T,G}(g) + +wrapped_graph(g::ReverseView) = g.g + +Graphs.is_directed(::ReverseView{T,G}) where {T,G} = true +Graphs.is_directed(::Type{<:ReverseView{T,G}}) where {T,G} = true + +Graphs.edgetype(g::ReverseView) = Graphs.edgetype(g.g) +Graphs.has_vertex(g::ReverseView, v) = Graphs.has_vertex(g.g, v) +Graphs.ne(g::ReverseView) = Graphs.ne(g.g) +Graphs.nv(g::ReverseView) = Graphs.nv(g.g) +Graphs.vertices(g::ReverseView) = Graphs.vertices(g.g) +Graphs.edges(g::ReverseView) = (reverse(e) for e in Graphs.edges(g.g)) +Graphs.has_edge(g::ReverseView, s, d) = Graphs.has_edge(g.g, d, s) +Graphs.inneighbors(g::ReverseView, v) = Graphs.outneighbors(g.g, v) +Graphs.outneighbors(g::ReverseView, v) = Graphs.inneighbors(g.g, v) + +""" + UndirectedView{T<:Integer,G<:AbstractGraph} <: AbstractGraph{T} + +A graph view that wraps a directed graph and consider every edge as undirected. + +!!! warning + Some properties of the view, such as the number of edges, are cached at + construction time. Modifying the underlying graph after constructing the view + will lead to incorrect results. + +# Examples +```jldoctest +julia> using Graphs + +julia> g = SimpleDiGraph(2); + +julia> add_edge!(g, 1, 2); + +julia> ug = UndirectedView(g); + +julia> neighbors(ug, 1) +1-element Vector{Int64}: + 2 + +julia> neighbors(ug, 2) +1-element Vector{Int64}: + 1 +``` +""" +struct UndirectedView{T<:Integer,G<:AbstractGraph} <: AbstractGraph{T} + g::G + ne::Int + @traitfn function UndirectedView{T,G}( + g::::(IsDirected) + ) where {T<:Integer,G<:AbstractGraph{T}} + ne = count(e -> src(e) <= dst(e) || !has_edge(g, dst(e), src(e)), Graphs.edges(g)) + return new(g, ne) + end + + @traitfn UndirectedView{T,G}(g::::(!IsDirected)) where {T<:Integer,G<:AbstractGraph{T}} = throw( + ArgumentError("Your graph needs to be directed") + ) +end + +UndirectedView(g::G) where {T<:Integer,G<:AbstractGraph{T}} = UndirectedView{T,G}(g) + +""" + wrapped_graph(g) + +Return the graph wrapped by `g` +""" +function wrapped_graph end + +wrapped_graph(g::UndirectedView) = g.g + +Graphs.is_directed(::UndirectedView) = false +Graphs.is_directed(::Type{<:UndirectedView}) = false + +Graphs.edgetype(g::UndirectedView) = Graphs.edgetype(g.g) +Graphs.has_vertex(g::UndirectedView, v) = Graphs.has_vertex(g.g, v) +Graphs.ne(g::UndirectedView) = g.ne +Graphs.nv(g::UndirectedView) = Graphs.nv(g.g) +Graphs.vertices(g::UndirectedView) = Graphs.vertices(g.g) +function Graphs.has_edge(g::UndirectedView, s, d) + return Graphs.has_edge(g.g, s, d) || Graphs.has_edge(g.g, d, s) +end +Graphs.inneighbors(g::UndirectedView, v) = Graphs.all_neighbors(g.g, v) +Graphs.outneighbors(g::UndirectedView, v) = Graphs.all_neighbors(g.g, v) +function Graphs.edges(g::UndirectedView) + return ( + begin + (u, v) = src(e), dst(e) + if (v < u) + (u, v) = (v, u) + end + Edge(u, v) + end for + e in Graphs.edges(g.g) if (src(e) <= dst(e) || !has_edge(g.g, dst(e), src(e))) + ) +end diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 000000000..e386e377c --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,23 @@ +[deps] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +ArnoldiMethod = "ec485272-7323-5ecc-a04f-4719b315124d" +Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" +Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +IGraphs = "647e90d3-2106-487c-adb4-c91fc07b96ea" +Inflate = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" +NautyGraphs = "7509a0a4-015a-4167-b44b-0799a1a2605e" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +SharedArrays = "1a1011a3-84de-559e-8e89-a11a2f7dc383" +SimpleTraits = "699a6c99-e7fa-54fc-8d76-47d257e15c1d" +SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" diff --git a/test/biconnectivity/articulation.jl b/test/biconnectivity/articulation.jl index 0ebaff9bd..837174063 100644 --- a/test/biconnectivity/articulation.jl +++ b/test/biconnectivity/articulation.jl @@ -22,18 +22,30 @@ art = @inferred(articulation(g)) ans = [1, 7, 8, 12] @test art == ans + @test art == findall(is_articulation.(Ref(g), vertices(g))) end for level in 1:6 btree = Graphs.binary_tree(level) for tree in test_generic_graphs(btree; eltypes=[Int, UInt8, Int16]) artpts = @inferred(articulation(tree)) - @test artpts == collect(1:(2^(level - 1) - 1)) + @test artpts == collect(1:(2 ^ (level - 1) - 1)) + @test artpts == findall(is_articulation.(Ref(tree), vertices(tree))) end end hint = blockdiag(wheel_graph(5), wheel_graph(5)) add_edge!(hint, 5, 6) for h in test_generic_graphs(hint; eltypes=[Int, UInt8, Int16]) - @test @inferred(articulation(h)) == [5, 6] + art = @inferred(articulation(h)) + @test art == [5, 6] + @test art == findall(is_articulation.(Ref(h), vertices(h))) end + + # graph with disconnected components + g = path_graph(5) + es = collect(edges(g)) + g2 = Graph(vcat(es, [Edge(e.src + nv(g), e.dst + nv(g)) for e in es])) + @test articulation(g) == [2, 3, 4] # a single connected component + @test articulation(g2) == [2, 3, 4, 7, 8, 9] # two identical connected components + @test articulation(g2) == findall(is_articulation.(Ref(g2), vertices(g2))) end diff --git a/test/centrality/eigenvector.jl b/test/centrality/eigenvector.jl index af13508b7..5fb69d9b3 100644 --- a/test/centrality/eigenvector.jl +++ b/test/centrality/eigenvector.jl @@ -4,8 +4,7 @@ for g in test_generic_graphs(g1) y = @inferred(eigenvector_centrality(g)) - @test round.(y, digits=3) == - round.( + @test round.(y, digits=3) == round.( [ 0.3577513877490464, 0.3577513877490464, diff --git a/test/community/cliques.jl b/test/community/cliques.jl index 85f4a8920..149564e1a 100644 --- a/test/community/cliques.jl +++ b/test/community/cliques.jl @@ -12,7 +12,12 @@ function test_cliques(graph, expected) # Make test results insensitive to ordering - return setofsets(@inferred(maximal_cliques(graph))) == setofsets(expected) + okay_maximal = setofsets(@inferred(maximal_cliques(graph))) == setofsets(expected) + okay_maximum = Set(@inferred(maximum_clique(graph))) in setofsets(expected) + okay_maximum2 = + length(@inferred(maximum_clique(graph))) == maximum(length.(expected)) + okay_number = @inferred(clique_number(graph)) == maximum(length.(expected)) + return okay_maximal && okay_maximum && okay_maximum2 && okay_number end gx = SimpleGraph(3) diff --git a/test/community/independent_sets.jl b/test/community/independent_sets.jl new file mode 100644 index 000000000..03b46a61e --- /dev/null +++ b/test/community/independent_sets.jl @@ -0,0 +1,34 @@ +################################################################## +# +# Maximal independent sets of undirected graph +# Derived from Graphs.jl: https://github.com/julialang/Graphs.jl +# +################################################################## + +@testset "Independent Sets" begin + function setofsets(array_of_arrays) + return Set(map(Set, array_of_arrays)) + end + + function test_independent_sets(graph, expected) + # Make test results insensitive to ordering + okay_maximal = + setofsets(@inferred(maximal_independent_sets(graph))) == setofsets(expected) + okay_maximum = Set(@inferred(maximum_independent_set(graph))) in setofsets(expected) + okay_maximum2 = + length(@inferred(maximum_independent_set(graph))) == maximum(length.(expected)) + okay_number = @inferred(independence_number(graph)) == maximum(length.(expected)) + return okay_maximal && okay_maximum && okay_maximum2 && okay_number + end + + gx = SimpleGraph(3) + add_edge!(gx, 1, 2) + for g in test_generic_graphs(gx) + @test test_independent_sets(g, Array[[1, 3], [2, 3]]) + end + add_edge!(gx, 2, 3) + for g in test_generic_graphs(gx) + @test test_independent_sets(g, Array[[1, 3], [2]]) + end + @test independence_number(cycle_graph(11)) == 5 +end diff --git a/test/community/louvain.jl b/test/community/louvain.jl new file mode 100644 index 000000000..032b3b530 --- /dev/null +++ b/test/community/louvain.jl @@ -0,0 +1,157 @@ +@testset "Louvain" begin + # Basic Test case + barbell = barbell_graph(3, 3) + c = [1, 1, 1, 2, 2, 2] + for g in test_generic_graphs(barbell) + # Should work regardless of rng + r = @inferred louvain(g) + @test c == r + end + + # Test clique + clique = complete_graph(10) + c = ones(10) + for g in test_generic_graphs(clique) + # Should work regardless of rng + r = @inferred louvain(g) + @test c == r + end + + # Test disconnected + disconnected = barbell_graph(4, 4) + rem_edge!(disconnected, 4, 5) + c = [1, 1, 1, 1, 2, 2, 2, 2] + for g in test_generic_graphs(disconnected) + # Should work regardless of rng + r = @inferred louvain(g) + @test c == r + end + + # Test Empty Graph + empty = SimpleGraph(10) + c = collect(1:10) + for g in test_generic_graphs(empty) + # Should work regardless of rng + r = @inferred louvain(g) + @test c == r + end + + # Test multiple merges + g = blockdiag(barbell_graph(3, 3), complete_graph(10)) + add_edge!(g, 2, 5) + c = [ones(6); 2*ones(10)] + # Should work regardless of rng + # generic_graphs uses UInt8 for T that is too small + r = @inferred louvain(g) + @test c == r + + # Test loops + loops = complete_graph(2) + add_edge!(loops, 1, 1) + add_edge!(loops, 2, 2) + c = [1, 2] + for g in test_generic_graphs(loops) + # Should work regardless of rng + r = @inferred louvain(g) + @test c == r + end + + # Test γ + g = complete_graph(2) + c1 = [1, 1] + c2 = [1, 2] + for g in test_generic_graphs(g) + # Should work regardless of rng + r = @inferred louvain(g) + @test c1 == r + r = @inferred louvain(g, γ=2) + @test c2 == r + end + + # Test custom distmx + square = CycleGraph(4) + d = [ + [0 4 0 1] + [4 0 1 0] + [0 1 0 4] + [1 0 4 0] + ] + c = [1, 1, 2, 2] + for g in test_generic_graphs(square) + # Should work regardless of rng + r = @inferred louvain(g, distmx=d) + @test c == r + end + + # Test max_merges + g = blockdiag(barbell_graph(3, 3), complete_graph(10)) + add_edge!(g, 2, 5) + c = [ones(3); 2*ones(3)] + # Should work regardless of rng + # generic_graphs uses UInt8 for T that is too small + r = @inferred louvain(g, max_merges=0) + @test c == r[1:6] + # the clique does not resolve in one step so we don't know what + # the coms will be. But we know the barbell splits into two groups + # of 3 in step one and merges in step two. + + # Directed cases + + # Simple + triangle = SimpleDiGraph(3) + add_edge!(triangle, 1, 2) + add_edge!(triangle, 2, 3) + add_edge!(triangle, 3, 1) + + barbell = blockdiag(triangle, triangle) + add_edge!(barbell, 1, 4) + c1 = [1, 1, 1, 2, 2, 2] + c2 = [1, 1, 1, 1, 1, 1] + for g in test_generic_graphs(barbell) + r = @inferred louvain(g) + @test r == c1 + r = @inferred louvain(g, γ=10e-5) + @test r == c2 + end + + # Self loops + barbell = SimpleDiGraph(2) + add_edge!(barbell, 1, 1) + add_edge!(barbell, 2, 2) + add_edge!(barbell, 1, 2) + c1 = [1, 2] + c2 = [1, 1] + for g in test_generic_graphs(barbell) + r = @inferred louvain(g) + @test r == c1 + r = @inferred louvain(g, γ=10e-5) + @test r == c2 + end + + # Weighted + square = SimpleDiGraph(4) + add_edge!(square, 1, 2) + add_edge!(square, 2, 3) + add_edge!(square, 3, 4) + add_edge!(square, 4, 1) + d1 = [ + [0 5 0 0] + [0 0 1 0] + [0 0 0 5] + [1 0 0 0] + ] + d2 = [ + [0 1 0 0] + [0 0 5 0] + [0 0 0 1] + [5 0 0 0] + ] + c1 = [1, 1, 2, 2] + c2 = [1, 2, 2, 1] + for g in test_generic_graphs(square) + r = @inferred louvain(g, distmx=d1) + @test r == c1 + r = @inferred louvain(g, distmx=d2) + @test r == c2 + end +end diff --git a/test/community/modularity.jl b/test/community/modularity.jl index 862e0aded..8bd0be080 100644 --- a/test/community/modularity.jl +++ b/test/community/modularity.jl @@ -24,6 +24,13 @@ @test isapprox(Q2, 0.6071428571428571, atol=1e-3) end + # Test with self loop + add_edge!(barbell, 1, 1) + for g in test_generic_graphs(barbell) + Q = @inferred(modularity(g, c)) + @test isapprox(Q, 0.3671875, atol=1e-3) + end + # 2. directed test cases triangle = SimpleDiGraph(3) add_edge!(triangle, 1, 2) @@ -65,6 +72,20 @@ Q = @inferred(modularity(g, c, distmx=d)) @test isapprox(Q, 0.045454545454545456, atol=1e-3) end + # Add self loop with weight 5 + add_edge!(barbell, 1, 1) + d = [ + [5 1 1 0 0 0] + [1 0 1 0 0 0] + [1 1 0 1 0 0] + [0 0 1 0 1 1] + [0 0 0 1 0 1] + [0 0 0 1 1 0] + ] + for g in test_generic_graphs(barbell) + Q = @inferred(modularity(g, c, distmx=d)) + @test isapprox(Q, 0.329861, atol=1e-3) + end # 3.2. directed and weighted test cases triangle = SimpleDiGraph(3) @@ -73,12 +94,12 @@ add_edge!(triangle, 3, 1) barbell = blockdiag(triangle, triangle) - add_edge!(barbell, 1, 4) # this edge has a weight of 5 + add_edge!(barbell, 3, 4) # this edge has a weight of 5 c = [1, 1, 1, 2, 2, 2] d = [ - [0 1 0 5 0 0] + [0 1 0 0 0 0] [0 0 1 0 0 0] - [1 0 0 0 0 0] + [1 0 0 5 0 0] [0 0 0 0 1 0] [0 0 0 0 0 1] [0 0 0 1 0 0] @@ -87,4 +108,18 @@ Q = @inferred(modularity(g, c, distmx=d)) @test isapprox(Q, 0.1487603305785124, atol=1e-3) end + # Add self loop with weight 5 + add_edge!(barbell, 1, 1) + d = [ + [5 1 0 0 0 0] + [0 0 1 0 0 0] + [1 0 0 1 0 0] + [0 0 0 0 1 0] + [0 0 0 0 0 1] + [0 0 0 1 0 0] + ] + for g in test_generic_graphs(barbell) + Q = @inferred(modularity(g, c, distmx=d)) + @test isapprox(Q, 0.333333333, atol=1e-3) + end end diff --git a/test/distance.jl b/test/distance.jl index da626bf6f..18e33faf0 100644 --- a/test/distance.jl +++ b/test/distance.jl @@ -2,8 +2,12 @@ g4 = path_digraph(5) adjmx1 = [0 1 0; 1 0 1; 0 1 0] # graph adjmx2 = [0 1 0; 1 0 1; 1 1 0] # digraph + adjmx3 = [0 1 0; 0 0 0; 0 0 0] a1 = SimpleGraph(adjmx1) a2 = SimpleDiGraph(adjmx2) + a3 = SimpleDiGraph(adjmx3) + a4 = blockdiag(complete_graph(5), complete_graph(5)); + add_edge!(a4, 1, 6) distmx1 = [Inf 2.0 Inf; 2.0 Inf 4.2; Inf 4.2 Inf] distmx2 = [Inf 2.0 Inf; 3.2 Inf 4.2; 5.5 6.1 Inf] @@ -44,6 +48,78 @@ @test @inferred(center(z)) == center(g, distmx2) == [2] end end + + @testset "Disconnected graph diameter" for g in test_generic_graphs(a3) + @test @inferred(diameter(g)) == typemax(Int) + end + + @testset "simplegraph diameter" for g in test_generic_graphs(a4) + @test @inferred(diameter(g)) == 3 + end + + @testset "Empty graph diameter" begin + @test @inferred(diameter(SimpleGraph(0))) == 0 + @test @inferred(diameter(SimpleDiGraph(0))) == 0 + end + + @testset "iFUB diameter" begin + # 1. Comparing against large graphs with known diameters + n_large = 5000 + g_path = path_graph(n_large) + @test diameter(g_path) == n_large - 1 + + g_cycle = cycle_graph(n_large) + @test diameter(g_cycle) == floor(Int, n_large / 2) + + g_star = star_graph(n_large) + @test diameter(g_star) == 2 + + # 2. Comparing against the original implementation for random graphs + function diameter_naive(g) + return maximum(eccentricity(g)) + end + + NUM_SAMPLES = 50 # Adjust this to change test duration + + # Silence the many "Infinite path length detected" warnings from + # eccentricity on disconnected random graphs. + with_logger(NullLogger()) do + for i in 1:NUM_SAMPLES + # Random unweighted Graphs + n = rand(10:1000) # Small to Medium size graphs + p = rand() * 0.1 + 0.005 # Sparse to medium density + + # Undirected Graphs + g = erdos_renyi(n, p) + @test diameter(g) == diameter_naive(g) + + ccs = connected_components(g) + largest_component = ccs[argmax(length.(ccs))] + g_lscc, _ = induced_subgraph(g, largest_component) + + if nv(g_lscc) > 1 + d_new = @inferred diameter(g_lscc) + d_ref = diameter_naive(g_lscc) + @test d_new == d_ref + end + + # Directed Graphs + g_dir = erdos_renyi(n, p, is_directed=true) + @test diameter(g_dir) == diameter_naive(g_dir) + + sccs = strongly_connected_components(g_dir) + largest_component_directed = sccs[argmax(length.(sccs))] + g_dir_lscc, _ = induced_subgraph(g_dir, largest_component_directed) + + if nv(g_dir_lscc) > 1 + d_new_dir = @inferred diameter(g_dir_lscc) + d_ref_dir = diameter_naive(g_dir_lscc) + @test d_new_dir == d_ref_dir + end + end + end + end + @testset "DefaultDistance" begin @test size(Graphs.DefaultDistance()) == (typemax(Int), typemax(Int)) d = @inferred(Graphs.DefaultDistance(3)) diff --git a/test/operators.jl b/test/operators.jl index 2bcda7624..c30c3eef2 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -268,9 +268,27 @@ for i in 3:4 @testset "Tensor Product: $g" for g in testgraphs(path_graph(i)) @test length(connected_components(tensor_product(g, g))) == 2 + @test count_connected_components(tensor_product(g, g)) == 2 end end + gx = SimpleGraph(10, 20) + gy = SimpleGraph(15, 34) + @testset "Graph product edge counts" for (g1, g2) in zip(testgraphs(gx), testgraphs(gy)) + v1 = nv(g1) + v2 = nv(g2) + e1 = ne(g1) + e2 = ne(g2) + # Edge counts from https://en.wikipedia.org/wiki/Graph_product + @test ne(cartesian_product(g1, g2)) == v1 * e2 + v2 * e1 + @test ne(tensor_product(g1, g2)) == 2 * e1 * e2 + @test ne(lexicographic_product(g1, g2)) == v1 * e2 + e1 * v2^2 + @test ne(strong_product(g1, g2)) == v1 * e2 + v2 * e1 + 2 * e1 * e2 + @test ne(disjunctive_product(g1, g2)) == v1^2 * e2 + e1 * v2^2 - 2 * e1 * e2 + @test ne(homomorphic_product(g1, g2)) == + v1 * v2 * (v2 - 1) / 2 + e1 * (v2^2 - 2 * e2) + end + ## test subgraphs ## gb = smallgraph(:bull) @@ -353,19 +371,8 @@ @test length(g) == 10000 end - @testset "Undirected Line Graph" begin - @testset "Undirected Cycle Graphs" begin - for n in 3:9 - g = cycle_graph(n) - lg = line_graph(g) # checking if lg is an n-cycle - @test nv(lg) == n - @test ne(lg) == n - @test is_connected(lg) - @test all(degree(lg, v) == 2 for v in vertices(lg)) - end - end - - @testset "Undirected Path Graphs" begin + @testset "Undirected line graph" begin + @testset "Undirected path graphs" begin for n in 2:9 g = path_graph(n) lg = line_graph(g) # checking if lg is an n-1-path @@ -377,7 +384,18 @@ end end - @testset "Undirected Star Graphs" begin + @testset "Undirected cycle graphs" begin + for n in 3:9 + g = cycle_graph(n) + lg = line_graph(g) # checking if lg is an n-cycle + @test nv(lg) == n + @test ne(lg) == n + @test is_connected(lg) + @test all(degree(lg, v) == 2 for v in vertices(lg)) + end + end + + @testset "Undirected star graphs" begin for n in 3:9 g = star_graph(n) lg = line_graph(g) # checking if lg is a complete graph on n-1 vertices @@ -386,10 +404,9 @@ end end - @testset "Undirected Self-loops" begin - for T in - (Int8, Int16, Int32, Int64, Int128, UInt8, UInt16, UInt32, UInt64, UInt128) - g = SimpleGraph{T}(2, [T[2], T[1, 2], T[]]) + @testset "Undirected self-loops" begin + for T in (Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64) + g = SimpleGraph{T}(2, [T[2], T[1, 2], T[]]) # vertices 1:3, edges 1-2, 2-2 lg = line_graph(g) @test nv(lg) == 2 # only 2 edges (self-loop counts once) @test ne(lg) == 1 # only connection between edge 1-2 and self-loop 2-2 @@ -397,8 +414,8 @@ end end - @testset "Directed Line Graph" begin - @testset "Directed Cycle Graphs" begin + @testset "Directed line graph" begin + @testset "Directed cycle craphs" begin for n in 3:9 g = cycle_digraph(n) lg = line_graph(g) @@ -411,7 +428,7 @@ end end - @testset "Directed Path Graphs" begin + @testset "Directed path graphs" begin for n in 2:9 g = path_digraph(n) lg = line_graph(g) @@ -419,12 +436,12 @@ @test ne(lg) == n - 2 @test is_directed(lg) @test is_connected(lg) - @test all(outdegree(lg, v) == (v < n - 1 ? 1 : 0) for v in vertices(lg)) + @test all(outdegree(lg, v) == (v < n-1 ? 1 : 0) for v in vertices(lg)) @test all(indegree(lg, v) == (v > 1 ? 1 : 0) for v in vertices(lg)) end end - @testset "Directed Star Graphs" begin + @testset "Directed star graphs" begin for m in 0:4, n in 0:4 g = SimpleDiGraph(m + n + 1) foreach(i -> add_edge!(g, i + 1, 1), 1:m) @@ -432,21 +449,22 @@ lg = line_graph(g) # checking if lg is the complete bipartite digraph @test nv(lg) == m + n @test ne(lg) == m * n - @test all(outdegree(lg, v) == 0 && indegree(lg, v) == m for v in 1:n) + @test all(outdegree(lg, v)==0 && indegree(lg, v)==m for v in 1:n) @test all( - outdegree(lg, v) == n && indegree(lg, v) == 0 for v in (n + 1):(n + m) + outdegree(lg, v)==n && indegree(lg, v)==0 for v in (n + 1):(n + m) ) end end - @testset "Directed Self-loops" begin - for T in - (Int8, Int16, Int32, Int64, Int128, UInt8, UInt16, UInt32, UInt64, UInt128) - g = SimpleDiGraph{T}(2, [T[1, 2], T[], T[]], [T[1], T[1], T[]]) - lg = line_graph(g) - @test nv(lg) == 2 - @test ne(lg) == 1 - @test has_edge(lg, 1, 2) + @testset "Directed self-loops" begin + for T in (Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64), + l in (:none, :inherit) + + g = SimpleDiGraph{T}(3, [T[2], T[2, 3], T[]], [T[], T[1, 2], T[2]]) # vertices 1:3, edges 1->2, 2->2, 2->3 + lg = line_graph(g, loops=l) + @test nv(lg) == 3 + @test ne(lg) == (l==:none ? 3 : 4) + @test l==:none || has_edge(lg, 2, 2) end end end diff --git a/test/parallel/distance.jl b/test/parallel/distance.jl index 16db54e06..115114ae5 100644 --- a/test/parallel/distance.jl +++ b/test/parallel/distance.jl @@ -1,4 +1,4 @@ -@testset "Parallel.Distance" begin +@testset "Parallel.Distance" for parallel in [:threads, :distributed] g4 = path_digraph(5) adjmx1 = [0 1 0; 1 0 1; 0 1 0] # graph adjmx2 = [0 1 0; 1 0 1; 1 1 0] # digraph @@ -9,7 +9,7 @@ for g in testgraphs(a1) z = @inferred(Graphs.eccentricity(g, distmx1)) - y = @inferred(Parallel.eccentricity(g, distmx1)) + y = @inferred(Parallel.eccentricity(g, distmx1; parallel)) @test isapprox(y, z) @test @inferred(Graphs.diameter(y)) == @inferred(Parallel.diameter(g, distmx1)) == @@ -21,9 +21,15 @@ @test @inferred(Graphs.center(y)) == @inferred(Parallel.center(g, distmx1)) == [2] end + let g = testgraphs(a1)[1] + # An error should be reported if the parallel mode could not be understood + @test_throws ArgumentError Parallel.eccentricity(g, distmx1; parallel=:thread) + @test_throws ArgumentError Parallel.eccentricity(g, distmx1; parallel=:distriibuted) + end + for g in testdigraphs(a2) z = @inferred(Graphs.eccentricity(g, distmx2)) - y = @inferred(Parallel.eccentricity(g, distmx2)) + y = @inferred(Parallel.eccentricity(g, distmx2; parallel)) @test isapprox(y, z) @test @inferred(Graphs.diameter(y)) == @inferred(Parallel.diameter(g, distmx2)) == diff --git a/test/parallel/runtests.jl b/test/parallel/runtests.jl index 76805ac5b..c9cc7ee43 100644 --- a/test/parallel/runtests.jl +++ b/test/parallel/runtests.jl @@ -1,5 +1,6 @@ using Graphs using Graphs.Parallel +using SharedArrays # to trigger extension loading using Base.Threads: @threads, Atomic @test length(description()) > 1 diff --git a/test/parallel/shortestpaths/dijkstra.jl b/test/parallel/shortestpaths/dijkstra.jl index 34f948232..ee26ce847 100644 --- a/test/parallel/shortestpaths/dijkstra.jl +++ b/test/parallel/shortestpaths/dijkstra.jl @@ -1,4 +1,4 @@ -@testset "Parallel.Dijkstra" begin +@testset "Parallel.Dijkstra" for parallel in [:threads, :distributed] g4 = path_digraph(5) d1 = float([0 1 2 3 4; 5 0 6 7 8; 9 10 0 11 12; 13 14 15 0 16; 17 18 19 20 0]) d2 = sparse(float([0 1 2 3 4; 5 0 6 7 8; 9 10 0 11 12; 13 14 15 0 16; 17 18 19 20 0])) @@ -8,7 +8,7 @@ for g in testgraphs(g3) z = floyd_warshall_shortest_paths(g, d) - zp = @inferred(Parallel.dijkstra_shortest_paths(g, collect(1:5), d)) + zp = @inferred(Parallel.dijkstra_shortest_paths(g, collect(1:5), d; parallel)) @test all(isapprox(z.dists, zp.dists)) for i in 1:5 @@ -21,7 +21,7 @@ end z = floyd_warshall_shortest_paths(g) - zp = @inferred(Parallel.dijkstra_shortest_paths(g)) + zp = @inferred(Parallel.dijkstra_shortest_paths(g; parallel)) @test all(isapprox(z.dists, zp.dists)) for i in 1:5 @@ -34,7 +34,7 @@ end z = floyd_warshall_shortest_paths(g) - zp = @inferred(Parallel.dijkstra_shortest_paths(g, [1, 2])) + zp = @inferred(Parallel.dijkstra_shortest_paths(g, [1, 2]; parallel)) @test all(isapprox(z.dists[1:2, :], zp.dists)) for i in 1:2 @@ -51,9 +51,17 @@ g3 = path_digraph(5) d = float([0 1 2 3 4; 5 0 6 7 8; 9 10 0 11 12; 13 14 15 0 16; 17 18 19 20 0]) + # An error should be reported if the parallel mode could not be understood + @test_throws ArgumentError Parallel.dijkstra_shortest_paths( + testdigraphs(g3)[1], collect(1:5), d; parallel=:thread + ) + @test_throws ArgumentError Parallel.dijkstra_shortest_paths( + testdigraphs(g3)[1], collect(1:5), d; parallel=:distriibuted + ) + for g in testdigraphs(g3) z = floyd_warshall_shortest_paths(g, d) - zp = @inferred(Parallel.dijkstra_shortest_paths(g, collect(1:5), d)) + zp = @inferred(Parallel.dijkstra_shortest_paths(g, collect(1:5), d; parallel)) @test all(isapprox(z.dists, zp.dists)) for i in 1:5 @@ -66,7 +74,7 @@ end z = floyd_warshall_shortest_paths(g) - zp = @inferred(Parallel.dijkstra_shortest_paths(g)) + zp = @inferred(Parallel.dijkstra_shortest_paths(g; parallel)) @test all(isapprox(z.dists, zp.dists)) for i in 1:5 @@ -79,7 +87,7 @@ end z = floyd_warshall_shortest_paths(g) - zp = @inferred(Parallel.dijkstra_shortest_paths(g, [1, 2])) + zp = @inferred(Parallel.dijkstra_shortest_paths(g, [1, 2]; parallel)) @test all(isapprox(z.dists[1:2, :], zp.dists)) for i in 1:2 diff --git a/test/parallel/traversals/greedy_color.jl b/test/parallel/traversals/greedy_color.jl index 65a43a514..65759ae23 100644 --- a/test/parallel/traversals/greedy_color.jl +++ b/test/parallel/traversals/greedy_color.jl @@ -1,8 +1,8 @@ -@testset "Parallel.Greedy Coloring" begin +@testset "Parallel.Greedy Coloring" for parallel in [:threads, :distributed] g3 = star_graph(10) for g in testgraphs(g3) for op_sort in (true, false) - C = @inferred(Parallel.greedy_color(g, reps=5, sort_degree=op_sort)) + C = @inferred(Parallel.greedy_color(g; reps=5, sort_degree=op_sort, parallel)) @test C.num_colors == 2 end end @@ -10,10 +10,22 @@ g4 = path_graph(20) g5 = complete_graph(20) + let g = testgraphs(g4)[1] + # An error should be reported if the parallel mode could not be understood + @test_throws ArgumentError Parallel.greedy_color( + g; reps=5, sort_degree=false, parallel=:thread + ) + @test_throws ArgumentError Parallel.greedy_color( + g; reps=5, sort_degree=false, parallel=:distriibuted + ) + end + for graph in [g4, g5] for g in testgraphs(graph) for op_sort in (true, false) - C = @inferred(Parallel.greedy_color(g, reps=5, sort_degree=op_sort)) + C = @inferred( + Parallel.greedy_color(g; reps=5, sort_degree=op_sort, parallel) + ) @test C.num_colors <= maximum(degree(g)) + 1 correct = true diff --git a/test/planarity.jl b/test/planarity.jl new file mode 100644 index 000000000..ab3571cc3 --- /dev/null +++ b/test/planarity.jl @@ -0,0 +1,121 @@ +@testset "Planarity tests" begin + @testset "LRP constructor tests" begin + function lrp_constructor_test(g) + try + lrp = Graphs.LRPlanarity(g) + return true + catch e + return false + end + end + g = SimpleGraph(10, 10) + @test lrp_constructor_test(g) + g = SimpleGraph{Int8}(10, 10) + @test lrp_constructor_test(g) + end + + @testset "DFS orientation tests" begin + dfs_test_edges = [ + (1, 2), + (1, 4), + (1, 6), + (2, 3), + (2, 4), + (2, 5), + (3, 5), + (3, 6), + (4, 5), + (4, 6), + (5, 6), + ] + + dfs_g = Graph(6) + for edge in dfs_test_edges + add_edge!(dfs_g, edge) + end + + self = Graphs.LRPlanarity(dfs_g) + #want to test dfs orientation + # make adjacency lists for dfs + for v in 1:(self.V) #for all vertices in G, + self.adjs[v] = neighbors(dfs_g, v) ##neighbourhood of v + end + T = eltype(dfs_g) + + # orientation of the graph by depth first search traversal + for v in 1:(self.V) + if self.height[v] == -one(T) #using -1 rather than nothing for type stability. + self.height[v] = zero(T) + push!(self.roots, v) + Graphs.dfs_orientation!(self, v) + end + end + + #correct data + parent_edges = Dict([ + (1, Edge(0, 0)), + (2, Edge(1, 2)), + (3, Edge(2, 3)), + (4, Edge(5, 4)), + (5, Edge(3, 5)), + (6, Edge(4, 6)), + ]) + + @test parent_edges == self.parent_edge + + correct_heights = Dict([(1, 0), (2, 1), (3, 2), (4, 4), (5, 3), (6, 5)]) + + @test correct_heights == self.height + end + + @testset "Planarity results" begin + #Construct example planar graph + planar_edges = [ + (1, 2), (1, 3), (2, 4), (2, 7), (3, 4), (3, 5), (4, 6), (4, 7), (5, 6) + ] + + g = Graph(7) + + for edge in planar_edges + add_edge!(g, edge) + end + + @test is_planar(g) == true + + #another planar graph + cl = circular_ladder_graph(8) + @test is_planar(cl) == true + + # one more planar graph + w = wheel_graph(10) + @test is_planar(w) == true + + #Construct some non-planar graphs + g = complete_graph(10) + @test is_planar(g) == false + + petersen = smallgraph(:petersen) + @test is_planar(petersen) == false + + d = smallgraph(:desargues) + @test is_planar(d) == false + + h = smallgraph(:heawood) + @test is_planar(h) == false + + mb = smallgraph(:moebiuskantor) + @test is_planar(mb) == false + + #Directed, planar example + dg = SimpleDiGraphFromIterator([Edge(1, 2), Edge(2, 3), Edge(3, 1)]) + + @test is_planar(dg) == true + end + + @testset "ManualDict tests" begin + md = Graphs.ManualDict(Int64, Int64, 0) + @test md[0] == 0 + md[0] = 1 + @test md[0] == 1 + end +end diff --git a/test/runtests.jl b/test/runtests.jl index bcac411b6..c4da7ef47 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,17 +1,19 @@ using Aqua using Documenter using Graphs -using Graphs.Experimental using Graphs.SimpleGraphs +using Graphs.Experimental +if isempty(VERSION.prerelease) + using JET +end using Graphs.Test -using JET -using JuliaFormatter using Test using SparseArrays using LinearAlgebra using DelimitedFiles using Base64 using Random +using Logging: NullLogger, with_logger using Statistics: mean, std using StableRNGs using Pkg @@ -80,6 +82,7 @@ tests = [ "interface", "core", "operators", + "wrappedGraphs/graphviews", "degeneracy", "distance", "digraph/transitivity", @@ -115,7 +118,9 @@ tests = [ "traversals/all_simple_paths", "community/cliques", "community/core-periphery", + "community/independent_sets", "community/label_propagation", + "community/louvain", "community/modularity", "community/clustering", "community/clique_percolation", @@ -134,6 +139,7 @@ tests = [ "spanningtrees/boruvka", "spanningtrees/kruskal", "spanningtrees/prim", + "spanningtrees/planar_maximally_filtered_graph", "steinertree/steiner_tree", "biconnectivity/articulation", "biconnectivity/biconnect", @@ -148,12 +154,11 @@ tests = [ "vertexcover/random_vertex_cover", "trees/prufer", "experimental/experimental", + "planarity", ] -args = lowercase.(ARGS) - @testset verbose = true "Graphs" begin - if "jet" in args || isempty(args) + if isempty(VERSION.prerelease) @testset "Code quality (JET.jl)" begin @assert get_pkg_version("JET") >= v"0.8.4" JET.test_package( @@ -165,27 +170,18 @@ args = lowercase.(ARGS) end end - if "aqua" in args || isempty(args) - @testset "Code quality (Aqua.jl)" begin - Aqua.test_all(Graphs; ambiguities=false) - end - end - - if "juliaformatter" in args || isempty(args) - @testset "Code formatting (JuliaFormatter.jl)" begin - @test format(Graphs; verbose=false, overwrite=false) - end + @testset "Code quality (Aqua.jl)" begin + Aqua.test_all(Graphs; ambiguities=false) end - if "doctest" in args || isempty(args) + if !Sys.iswindows() doctest(Graphs) end @testset verbose = true "Actual tests" begin for t in tests - if t in args || isempty(args) - include(joinpath(testdir, "$(t).jl")) - end + tp = joinpath(testdir, "$(t).jl") + include(tp) end end end; diff --git a/test/simplegraphs/generators/staticgraphs.jl b/test/simplegraphs/generators/staticgraphs.jl index 17107adce..b1e7c9cf4 100644 --- a/test/simplegraphs/generators/staticgraphs.jl +++ b/test/simplegraphs/generators/staticgraphs.jl @@ -411,6 +411,23 @@ @test isvalid_simplegraph(g) end + @testset "Regular Trees" begin + g = @inferred(regular_tree(3, 3)) + I = [1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] + J = [2, 3, 4, 1, 5, 6, 7, 8, 1, 9, 10, 1, 11, 12, 13, 2, 2, 2, 3, 3, 3, 4, 4, 4] + V = ones(Int, length(I)) + Adj = sparse(I, J, V) + @test Adj == sparse(g) + @test isvalid_simplegraph(g) + @test_throws InexactError regular_tree(Int8, 4, 5) + g = @inferred(regular_tree(Int16, 4, 5)) + @test isvalid_simplegraph(g) + # test that z = 1 recovers a path graph + @test all(regular_tree(k, 1) == path_graph(k) for k in 0:10) + # test that z = 2 recovers a binary tree + @test all(regular_tree(k, 2) == binary_tree(k) for k in 0:10) + end + @testset "Roach Graphs" begin rg3 = @inferred(roach_graph(3)) # [3] diff --git a/test/simplegraphs/runtests.jl b/test/simplegraphs/runtests.jl index 576110f14..7ae54ae5c 100644 --- a/test/simplegraphs/runtests.jl +++ b/test/simplegraphs/runtests.jl @@ -1,13 +1,13 @@ using Graphs.SimpleGraphs import Graphs.SimpleGraphs: fadj, badj, adj -import Graphs.edgetype, Graphs.has_edge +import Graphs: edgetype, has_edge using Statistics: mean struct DummySimpleGraph <: AbstractSimpleGraph{Int} end struct DummySimpleEdge <: AbstractSimpleEdge{Int} end DummySimpleEdge(x...) = DummySimpleEdge() -Graphs.edgetype(g::DummySimpleGraph) = DummySimpleEdge +edgetype(g::DummySimpleGraph) = DummySimpleEdge has_edge(::DummySimpleGraph, ::DummySimpleEdge) = true # function to check if the invariants for SimpleGraph and SimpleDiGraph holds diff --git a/test/simplegraphs/simplegraphs.jl b/test/simplegraphs/simplegraphs.jl index 78b5145ef..d9b979c6d 100644 --- a/test/simplegraphs/simplegraphs.jl +++ b/test/simplegraphs/simplegraphs.jl @@ -103,6 +103,7 @@ using Graphs.Test @test @inferred(edgetype(g)) == SimpleGraphEdge{T} @test @inferred(copy(g)) == g + @test @inferred(hash(g)) == hash(copy(g)) == hash(deepcopy(g)) @test @inferred(!is_directed(g)) e = first(edges(g)) @@ -124,6 +125,8 @@ using Graphs.Test @test @inferred(!has_edge(g, 20, 3)) @test @inferred(!has_edge(g, 2, 30)) + @test @inferred(copy(g)) == g + @test @inferred(hash(copy(g))) == hash(g) gc = copy(g) @test @inferred(add_edge!(gc, 4 => 1)) && gc == cycle_digraph(4) @test @inferred(has_edge(gc, 4 => 1)) && has_edge(gc, 0x04 => 0x01) @@ -322,8 +325,9 @@ using Graphs.Test SimpleGraph(0) ) E = edgetype(g) - edge_list = - E.([(4, 4), (1, 2), (4, 4), (1, 2), (4, 4), (2, 1), (0, 1), (1, 0), (0, 0)]) + edge_list = E.([ + (4, 4), (1, 2), (4, 4), (1, 2), (4, 4), (2, 1), (0, 1), (1, 0), (0, 0) + ]) edge_iter = (e for e in edge_list) edge_set = Set(edge_list) edge_set_any = Set{Any}(edge_list) @@ -352,8 +356,9 @@ using Graphs.Test SimpleDiGraph(0) ) E = edgetype(g) - edge_list = - E.([(4, 4), (1, 2), (4, 4), (1, 2), (4, 4), (2, 1), (0, 1), (1, 0), (0, 0)]) + edge_list = E.([ + (4, 4), (1, 2), (4, 4), (1, 2), (4, 4), (2, 1), (0, 1), (1, 0), (0, 0) + ]) edge_iter = (e for e in edge_list) edge_set = Set(edge_list) edge_set_any = Set{Any}(edge_list) diff --git a/test/spanningtrees/boruvka.jl b/test/spanningtrees/boruvka.jl index dfabbaebe..552b77bea 100644 --- a/test/spanningtrees/boruvka.jl +++ b/test/spanningtrees/boruvka.jl @@ -21,14 +21,18 @@ g1t = GenericGraph(SimpleGraph(edges1)) @test res1.weight == cost_mst # acyclic graphs have n - c edges - @test nv(g1t) - length(connected_components(g1t)) == ne(g1t) + @test nv(g1t) - ne(g1t) == + length(connected_components(g1t)) == + count_connected_components(g1t) @test nv(g1t) == nv(g) res2 = boruvka_mst(g, distmx; minimize=false) edges2 = [Edge(src(e), dst(e)) for e in res2.mst] g2t = GenericGraph(SimpleGraph(edges2)) @test res2.weight == cost_max_vec_mst - @test nv(g2t) - length(connected_components(g2t)) == ne(g2t) + @test nv(g2t) - ne(g2t) == + length(connected_components(g2t)) == + count_connected_components(g2t) @test nv(g2t) == nv(g) end # second test @@ -60,14 +64,18 @@ edges3 = [Edge(src(e), dst(e)) for e in res3.mst] g3t = GenericGraph(SimpleGraph(edges3)) @test res3.weight == weight_vec2 - @test nv(g3t) - length(connected_components(g3t)) == ne(g3t) + @test nv(g3t) - ne(g3t) == + length(connected_components(g3t)) == + count_connected_components(g3t) @test nv(g3t) == nv(gx) res4 = boruvka_mst(g, distmx_sec; minimize=false) edges4 = [Edge(src(e), dst(e)) for e in res4.mst] g4t = GenericGraph(SimpleGraph(edges4)) @test res4.weight == weight_max_vec2 - @test nv(g4t) - length(connected_components(g4t)) == ne(g4t) + @test nv(g4t) - ne(g4t) == + length(connected_components(g4t)) == + count_connected_components(g4t) @test nv(g4t) == nv(gx) end @@ -123,14 +131,18 @@ edges5 = [Edge(src(e), dst(e)) for e in res5.mst] g5t = GenericGraph(SimpleGraph(edges5)) @test res5.weight == weight_vec3 - @test nv(g5t) - length(connected_components(g5t)) == ne(g5t) + @test nv(g5t) - ne(g5t) == + length(connected_components(g5t)) == + count_connected_components(g5t) @test nv(g5t) == nv(gd) res6 = boruvka_mst(g, distmx_third; minimize=false) edges6 = [Edge(src(e), dst(e)) for e in res6.mst] g6t = GenericGraph(SimpleGraph(edges6)) @test res6.weight == weight_max_vec3 - @test nv(g6t) - length(connected_components(g6t)) == ne(g6t) + @test nv(g6t) - ne(g6t) == + length(connected_components(g6t)) == + count_connected_components(g6t) @test nv(g6t) == nv(gd) end end diff --git a/test/spanningtrees/kruskal.jl b/test/spanningtrees/kruskal.jl index ddf8fead8..a313c028e 100644 --- a/test/spanningtrees/kruskal.jl +++ b/test/spanningtrees/kruskal.jl @@ -8,6 +8,8 @@ 6 10 3 0 ] + weight_vector = [distmx[src(e), dst(e)] for e in edges(g4)] + vec_mst = Vector{Edge}([Edge(1, 2), Edge(3, 4), Edge(2, 3)]) max_vec_mst = Vector{Edge}([Edge(2, 4), Edge(1, 4), Edge(1, 3)]) for g in test_generic_graphs(g4) @@ -18,7 +20,15 @@ # so instead we compare tuples of source and target vertices @test sort([(src(e), dst(e)) for e in mst]) == sort([(src(e), dst(e)) for e in vec_mst]) @test sort([(src(e), dst(e)) for e in max_mst]) == sort([(src(e), dst(e)) for e in max_vec_mst]) + # test equivalent vector form + mst_vec = @inferred(kruskal_mst(g, weight_vector)) + max_mst_vec = @inferred(kruskal_mst(g, weight_vector, minimize=false)) + @test src.(mst_vec) == src.(mst) + @test dst.(mst_vec) == dst.(mst) + @test src.(max_mst_vec) == src.(max_mst) + @test dst.(max_mst_vec) == dst.(max_mst) end + # second test distmx_sec = [ 0 0 0.26 0 0.38 0 0.58 0.16 @@ -32,6 +42,8 @@ ] gx = SimpleGraph(distmx_sec) + weight_vector_sec = [distmx_sec[src(e), dst(e)] for e in edges(gx)] + vec2 = Vector{Edge}([ Edge(1, 8), Edge(3, 4), Edge(2, 8), Edge(1, 3), Edge(6, 8), Edge(5, 6), Edge(3, 7) ]) @@ -40,9 +52,15 @@ ]) for g in test_generic_graphs(gx) mst2 = @inferred(kruskal_mst(g, distmx_sec)) + mst2_vec = @inferred(kruskal_mst(g, weight_vector_sec)) max_mst2 = @inferred(kruskal_mst(g, distmx_sec, minimize=false)) + max_mst2_vec = @inferred(kruskal_mst(g, weight_vector_sec, minimize=false)) @test sort([(src(e), dst(e)) for e in mst2]) == sort([(src(e), dst(e)) for e in vec2]) @test sort([(src(e), dst(e)) for e in max_mst2]) == sort([(src(e), dst(e)) for e in max_vec2]) + @test src.(mst2) == src.(mst2_vec) + @test dst.(mst2) == dst.(mst2_vec) + @test src.(max_mst2) == src.(max_mst2_vec) + @test dst.(max_mst2) == dst.(max_mst2_vec) end # non regression test for #362 diff --git a/test/spanningtrees/planar_maximally_filtered_graph.jl b/test/spanningtrees/planar_maximally_filtered_graph.jl new file mode 100644 index 000000000..72c9a3220 --- /dev/null +++ b/test/spanningtrees/planar_maximally_filtered_graph.jl @@ -0,0 +1,49 @@ +@testset "PMFG tests" begin + k5 = complete_graph(5) + k5_am = Matrix{Float64}(adjacency_matrix(k5)) + + #random weights + for i in CartesianIndices(k5_am) + if k5_am[i] == 1 + k5_am[i] = rand() + end + end + + #let's make 1->5 very distant + k5_am[1, 5] = 10.0 + k5_am[5, 1] = 10.0 + + #correct result of PMFG + correct_am = [ + 0 1 1 1 0 + 1 0 1 1 1 + 1 1 0 1 1 + 1 1 1 0 1 + 0 1 1 1 0 + ] + + @test correct_am == adjacency_matrix(planar_maximally_filtered_graph(k5, k5_am)) + + #type test + N = 10 + g = SimpleGraph{Int16}(N) + X = rand(N, N) + C = X' * X + p = planar_maximally_filtered_graph(g, C) + @test typeof(p) == SimpleGraph{eltype(g)} + + #Test that MST is a subset of the PMFG + N = 50 + X = rand(N, N) + D = X' * X + c = complete_graph(N) + p = planar_maximally_filtered_graph(c, D) + mst_edges = kruskal_mst(c, D) + is_subgraph = true + for mst_edge in mst_edges + if mst_edge ∉ edges(p) + is_subgraph = false + end + end + @test is_subgraph +end diff --git a/test/wrappedGraphs/graphviews.jl b/test/wrappedGraphs/graphviews.jl new file mode 100644 index 000000000..d03b2ec8a --- /dev/null +++ b/test/wrappedGraphs/graphviews.jl @@ -0,0 +1,105 @@ +@testset "Graph Views" begin + @testset "ReverseView" begin + gx = DiGraph([ + Edge(1, 1), + Edge(1, 2), + Edge(1, 4), + Edge(2, 1), + Edge(2, 2), + Edge(2, 4), + Edge(3, 1), + Edge(4, 3), + ]) + + gr = erdos_renyi(20, 0.1; is_directed=true) + + for g in hcat(test_generic_graphs(gx), test_generic_graphs(gr)) + rg = ReverseView(g) + allocated_rg = DiGraph(nv(g)) + for e in edges(g) + add_edge!(allocated_rg, Edge(dst(e), src(e))) + end + + @test wrapped_graph(rg) == g + @test is_directed(rg) == true + @test eltype(rg) == eltype(g) + @test edgetype(rg) == edgetype(g) + @test has_vertex(rg, 4) == has_vertex(g, 4) + @test nv(rg) == nv(g) == nv(allocated_rg) + @test ne(rg) == ne(g) == ne(allocated_rg) + @test all(adjacency_matrix(rg) .== adjacency_matrix(allocated_rg)) + @test sort(collect(inneighbors(rg, 2))) == + sort(collect(inneighbors(allocated_rg, 2))) + @test sort(collect(outneighbors(rg, 2))) == + sort(collect(outneighbors(allocated_rg, 2))) + @test indegree(rg, 3) == indegree(allocated_rg, 3) + @test degree(rg, 1) == degree(allocated_rg, 1) + @test has_edge(rg, 1, 3) == has_edge(allocated_rg, 1, 3) + @test has_edge(rg, 1, 4) == has_edge(allocated_rg, 1, 4) + + rg_res = @inferred(floyd_warshall_shortest_paths(rg)) + allocated_rg_res = floyd_warshall_shortest_paths(allocated_rg) + @test rg_res.dists == allocated_rg_res.dists # parents may not be the same + + rg_res = @inferred(strongly_connected_components(rg)) + allocated_rg_res = strongly_connected_components(allocated_rg) + @test length(rg_res) == length(allocated_rg_res) + @test sort(length.(rg_res)) == sort(length.(allocated_rg_res)) + end + + @test_throws ArgumentError ReverseView(path_graph(5)) + end + + @testset "UndirectedView" begin + gx = DiGraph([ + Edge(1, 1), + Edge(1, 2), + Edge(1, 4), + Edge(2, 1), + Edge(2, 2), + Edge(2, 4), + Edge(3, 1), + Edge(4, 3), + ]) + + gr = erdos_renyi(20, 0.05; is_directed=true) + + for g in test_generic_graphs(gx) + ug = UndirectedView(g) + @test ne(ug) == 7 # one less edge since there was two edges in reverse directions + end + + for g in hcat(test_generic_graphs(gx), test_generic_graphs(gr)) + ug = UndirectedView(g) + allocated_ug = Graph(g) + + @test wrapped_graph(ug) == g + @test is_directed(ug) == false + @test eltype(ug) == eltype(g) + @test edgetype(ug) == edgetype(g) + @test has_vertex(ug, 4) == has_vertex(g, 4) + @test nv(ug) == nv(g) == nv(allocated_ug) + @test ne(ug) == ne(allocated_ug) + @test all(adjacency_matrix(ug) .== adjacency_matrix(allocated_ug)) + @test sort(collect(inneighbors(ug, 2))) == + sort(collect(inneighbors(allocated_ug, 2))) + @test sort(collect(outneighbors(ug, 2))) == + sort(collect(outneighbors(allocated_ug, 2))) + @test indegree(ug, 3) == indegree(allocated_ug, 3) + @test degree(ug, 1) == degree(allocated_ug, 1) + @test has_edge(ug, 1, 3) == has_edge(allocated_ug, 1, 3) + @test has_edge(ug, 1, 4) == has_edge(allocated_ug, 1, 4) + + ug_res = @inferred(floyd_warshall_shortest_paths(ug)) + allocated_ug_res = floyd_warshall_shortest_paths(allocated_ug) + @test ug_res.dists == allocated_ug_res.dists # parents may not be the same + + ug_res = @inferred(biconnected_components(ug)) + allocated_ug_res = biconnected_components(allocated_ug) + @test length(ug_res) == length(allocated_ug_res) + @test sort(length.(ug_res)) == sort(length.(allocated_ug_res)) + end + + @test_throws ArgumentError UndirectedView(path_graph(5)) + end +end