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 2135f4172..4097042e9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,11 +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 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 a40a78168..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,9 +161,14 @@ export join, tensor_product, cartesian_product, + strong_product, + disjunctive_product, + lexicographic_product, + homomorphic_product, crosspath, induced_subgraph, egonet, + line_graph, merge_vertices!, merge_vertices, @@ -207,6 +213,8 @@ export # connectivity connected_components, + connected_components!, + count_connected_components, strongly_connected_components, strongly_connected_components_kosaraju, strongly_connected_components_tarjan, @@ -319,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, @@ -341,6 +355,7 @@ export cycle_digraph, binary_tree, double_binary_tree, + regular_tree, roach_graph, clique_graph, ladder_graph, @@ -416,6 +431,7 @@ export # biconnectivity and articulation points articulation, + is_articulation, biconnected_components, bridges, @@ -435,8 +451,11 @@ export vertex_cover, # longestpaths - dag_longest_path + dag_longest_path, + # planarity + is_planar, + planar_maximally_filtered_graph """ Graphs @@ -461,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 """ @@ -532,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") @@ -557,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 d8aeb2172..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 ### """ @@ -879,3 +1105,150 @@ function merge_vertices!(g::Graph{T}, vs::Vector{U} where {U<:Integer}) where {T return new_vertex_ids end + +""" + 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 `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); # path with 4 edges + +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; loops::Symbol=:none) + @assert loops in (:none, :inherit, :all) + vertex_to_edges = [Int[] for _ in 1:nv(g)] + is_loop = BitVector(undef, ne(g)) + for (k, e) in enumerate(edges(g)) + s, d = src(e), dst(e) + 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 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 + + 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; 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 `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 = 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) # 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; 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 (k, e) in enumerate(edges(g)) + s, d = src(e), dst(e) + push!(vertex_to_edgesout[s], k) + push!(vertex_to_edgesin[d], k) + end + + 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 && loops == :none && continue # a self-loop in g does not induce a self-loop in lg + m += 1 + push!(fadjlist[ei], eo) + push!(badjlist[eo], ei) + end + end + + 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 bf4931ebf..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) @@ -352,4 +370,102 @@ @testset "Length: $(typeof(g))" for g in test_generic_graphs(SimpleGraph(100)) @test length(g) == 10000 end + + @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 + @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 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 + @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 + 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 + end + end + end + + @testset "Directed line graph" begin + @testset "Directed cycle craphs" 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 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 + 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) == (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 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 + 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 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 34bd1c53f..c4da7ef47 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3,8 +3,9 @@ using Documenter using Graphs using Graphs.SimpleGraphs using Graphs.Experimental -using JET -using JuliaFormatter +if isempty(VERSION.prerelease) + using JET +end using Graphs.Test using Test using SparseArrays @@ -12,6 +13,7 @@ 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,29 +154,30 @@ tests = [ "vertexcover/random_vertex_cover", "trees/prufer", "experimental/experimental", + "planarity", ] @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 isempty(VERSION.prerelease) + @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) end - @testset "Code formatting (JuliaFormatter.jl)" begin - @test format(Graphs; verbose=false, overwrite=false) + if !Sys.iswindows() + doctest(Graphs) end - doctest(Graphs) - @testset verbose = true "Actual tests" begin for t in tests tp = joinpath(testdir, "$(t).jl") 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