From c37e0fa8bd2350431ff2f1ae4cf6b8c41b701a3b Mon Sep 17 00:00:00 2001 From: stecrotti Date: Fri, 27 Sep 2024 11:33:52 +0200 Subject: [PATCH] add generators for bipartite graphs --- Project.toml | 8 +-- src/IndexedGraphs.jl | 10 +++- src/bipartiteindexedgraph.jl | 94 +++++++++++++++++++++++++++++++++++- test/bipartite.jl | 40 ++++++++++++++- 4 files changed, 145 insertions(+), 7 deletions(-) diff --git a/Project.toml b/Project.toml index cea276f..fc5244e 100644 --- a/Project.toml +++ b/Project.toml @@ -6,17 +6,19 @@ version = "0.5.1" [deps] Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" TrackingHeaps = "d94bfb22-46ad-56c3-87d0-f6c298cb800e" [compat] -Graphs = "1.4 - 1.9" +Graphs = "1.4" TrackingHeaps = "0.1" julia = "1" [extras] -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test", "Graphs"] +test = ["Test", "Graphs", "Random"] diff --git a/src/IndexedGraphs.jl b/src/IndexedGraphs.jl index e205c80..863e4de 100644 --- a/src/IndexedGraphs.jl +++ b/src/IndexedGraphs.jl @@ -5,13 +5,18 @@ using SparseArrays: sparse, SparseMatrixCSC, nnz, nzrange, rowvals, nonzeros, sp using Graphs: Graphs, AbstractGraph, SimpleGraph, AbstractSimpleGraph, AbstractEdge, src, dst, edgetype, has_vertex, has_edge, ne, nv, edges, vertices, neighbors, inneighbors, outneighbors, is_directed, is_bipartite, - bipartite_map, DijkstraState, dijkstra_shortest_paths + bipartite_map, DijkstraState, dijkstra_shortest_paths, + prufer_decode import Graphs: degree using Graphs.LinAlg using LinearAlgebra: LinearAlgebra, issymmetric +using Random: AbstractRNG, default_rng + +using StatsBase: sample + using TrackingHeaps: TrackingHeap, pop!, NoTrainingWheels, MinHeapOrder export @@ -31,7 +36,8 @@ export nv_left, nv_right, vertex, linearindex, vertices_left, vertices_right, vertex_left, vertex_right, is_directed, issymmetric, - bidirected_with_mappings + bidirected_with_mappings, + rand_bipartite_graph, rand_regular_bipartite_graph, rand_bipartite_tree """ AbstractIndexedEdge{T<:Integer} <: AbstractEdge{T} diff --git a/src/bipartiteindexedgraph.jl b/src/bipartiteindexedgraph.jl index c6d8041..95a33c1 100644 --- a/src/bipartiteindexedgraph.jl +++ b/src/bipartiteindexedgraph.jl @@ -309,4 +309,96 @@ function Graphs.adjacency_matrix(g::BipartiteIndexedGraph, T::DataType=Int) A = SparseMatrixCSC(g.A.m, g.A.n, g.A.colptr, g.A.rowval, ones(T, nnz(g.A))) return [ spzeros(T, m, m) A ; A' spzeros(T, n, n) ] -end \ No newline at end of file +end + + +##### GENERATORS + +""" + rand_bipartite_graph([rng=default_rng()], nleft, nright, ned) + +Create a bipartite graph with `nleft` nodes in the left block, `nright` nodes in the right block and `ned` edges taken uniformly at random. +""" +function rand_bipartite_graph(rng::AbstractRNG, nleft::Integer, nright::Integer, ned::Integer) + nleft > 0 || throw(ArgumentError("Number of variable nodes must be positive, got $nleft")) + nright > 0 || throw(ArgumentError("Number of left nodes must be positive, got $nright")) + ned > 0 || throw(ArgumentError("Number of edges must be positive, got $ned")) + nedmax = nleft * nright + ned ≤ nedmax || throw(ArgumentError("Maximum number of edges is $nleft*$nright=$nedmax, got $ned")) + + I = zeros(Int, ned) + J = zeros(Int, ned) + K = ones(Int, ned) + n = 1 + while n ≤ ned + I[n] = rand(rng, 1:nleft) + J[n] = rand(rng, 1:nright) + if !any((i,j) == (I[n], J[n]) for (i,j) in Iterators.take(zip(I,J), n-1)) + n += 1 + end + end + A = sparse(I, J, K, nleft, nright) + return BipartiteIndexedGraph(A) +end +function rand_bipartite_graph(nleft::Integer, nright::Integer, ned::Integer) + rand_bipartite_graph(default_rng(), nleft, nright, ned) +end + +""" + rand_bipartite_graph([rng=default_rng()], nleft, nright, p) + +Create a bipartite graph with `nleft` nodes in the left block, `nright` nodes in the right block and edges taken independently with probability `p`. +""" +function rand_bipartite_graph(rng::AbstractRNG, nright::Integer, nleft::Integer, p::Real) + nright > 0 || throw(ArgumentError("Number of right nodes must be positive, got $nright")) + nleft > 0 || throw(ArgumentError("Number of left nodes must be positive, got $nleft")) + 0 ≤ p ≤ 1 || throw(ArgumentError("Probability must be in [0,1], got $ned")) + + I = zeros(Int, 0) + J = zeros(Int, 0) + for (a, i) in Iterators.product(1:nleft, 1:nright) + if rand(rng) < p + push!(I, a) + push!(J, i) + end + end + K = ones(Int, length(I)) + A = sparse(I, J, K, nleft, nright) + return BipartiteIndexedGraph(A) +end +function rand_bipartite_graph(nright::Integer, nleft::Integer, p::Real) + rand_bipartite_graph(default_rng(), nleft, nright, p) +end + +""" + rand_regular_bipartite_graph([rng=default_rng()], nleft, nright, k) + +Create a bipartite graph with `nleft` nodes in the left block, `nright` nodes in the right block, where all left nodes have degree `k`. +""" +function rand_regular_bipartite_graph(rng::AbstractRNG, nright::Integer, nleft::Integer, + k::Integer) + nright > 0 || throw(ArgumentError("Number of right nodes must be positive, got $nright")) + nleft > 0 || throw(ArgumentError("Number of left nodes must be positive, got $nleft")) + k > 0 || throw(ArgumentError("Degree `k` must be positive, got $k")) + k ≤ nright || throw(ArgumentError("Degree `k` must be smaller or equal than number of rights, got $k")) + + I = reduce(vcat, fill(a, k) for a in 1:nleft) + J = reduce(vcat, sample(rng, 1:nright, k; replace=false) for _ in 1:nleft) + K = ones(Int, length(I)) + A = sparse(I, J, K, nleft, nright) + return BipartiteIndexedGraph(A) +end +function rand_regular_bipartite_graph(nright::Integer, nleft::Integer, k::Integer) + rand_regular_bipartite_graph(default_rng(), nleft, nright, k) +end + +""" + rand_bipartite_tree([rng=default_rng()], n) + +Create a tree bipartite graph with `n` vertices in total. The proportion of left/right nodes is random. +""" +function rand_bipartite_tree(rng::AbstractRNG, n::Integer) + gg = prufer_decode(rand(rng, 1:n, n-2)) + return BipartiteIndexedGraph(gg) +end +rand_bipartite_tree(n::Integer) = rand_bipartite_tree(default_rng(), n) \ No newline at end of file diff --git a/test/bipartite.jl b/test/bipartite.jl index 660aa1b..13a63ee 100644 --- a/test/bipartite.jl +++ b/test/bipartite.jl @@ -1,4 +1,4 @@ -using Graphs, IndexedGraphs, SparseArrays +using Graphs, IndexedGraphs, SparseArrays, Random @testset "bipartite graph" begin nl = 15 @@ -64,4 +64,42 @@ using Graphs, IndexedGraphs, SparseArrays db = dijkstra_shortest_paths(gb, sources, distvec) @test all(getproperty(d, p) == getproperty(db, p) for p in fieldnames(typeof(d))) end + + @testset "bipartite generators" begin + rng = MersenneTwister(0) + ngraphs = 20 + nrights = rand(rng, 5:50, ngraphs) + nlefts = rand(rng, 5:50, ngraphs) + es = [rand(rng, 1:n*m) for (n, m) in zip(nrights, nlefts)] + + @testset "Random bipartite graph - fixed # edges" begin + @test all(zip(nrights, nlefts, es)) do (n, m, e) + g = rand_bipartite_graph(rng, m, n, e) + nv_right(g) == n && nv_left(g) == m && ne(g) == e + end + end + + @testset "Random bipartite graph - prob of edges" begin + p = 0.1 + @test all(zip(nrights, nlefts)) do (n, m) + g = rand_bipartite_graph(rng, n, m, p) + nv_right(g) == n && nv_left(g) == m + end + end + + @testset "Random regular bipartite graph" begin + k = 4 + @test all(zip(nrights, nlefts)) do (n, m) + g = rand_regular_bipartite_graph(rng, n, m, k) + nv_right(g) == n && nv_left(g) == m && ne(g) == m * k + end + end + + @testset "Random bipartite tree" begin + @test all(nrights) do n + g = rand_bipartite_tree(rng, n) + nv(g) == n && !is_cyclic(g) + end + end + end end \ No newline at end of file