Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Havel-Hakimi and Kleitman-Wang graph realization algorithms #202

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
3 changes: 3 additions & 0 deletions src/SimpleGraphs/SimpleGraphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ using SparseArrays
using LinearAlgebra
using Graphs
using SimpleTraits
using DataStructures: OrderedDict

import Random: AbstractRNG

Expand Down Expand Up @@ -62,6 +63,8 @@ export AbstractSimpleGraph,
random_regular_graph,
random_regular_digraph,
random_configuration_model,
havel_hakimi_graph_generator,
kleitman_wang_graph_generator,
uniform_tree,
random_tournament_digraph,
StochasticBlockModel,
Expand Down
138 changes: 138 additions & 0 deletions src/SimpleGraphs/generators/randgraphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,144 @@ function uniform_tree(n::Integer; rng::Union{Nothing,AbstractRNG}=nothing)
return prufer_decode(random_code)
end

"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if randgraphs.jl is the correct place for these functions, maybe staticgraphs.jl is a better place, as generators are not random.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We moved it to staticgraphs.jl. Thanks for the suggestion.

havel_hakimi_graph_generator(degree_sequence::AbstractVector{<:Integer})

Returns a simple graph with a given finite degree sequence of non-negative integers generated via the Havel-Hakimi algorithm which works as follows:
1. successively connect the node of highest degree to other nodes of highest degree;
2. sort the remaining nodes by degree in decreasing order;
3. repeat the procedure.

## References
1. [Hakimi (1962)](https://doi.org/10.1137/0110037);
2. [Wikipedia](https://en.wikipedia.org/wiki/Havel%E2%80%93Hakimi_algorithm).
"""
function havel_hakimi_graph_generator(degree_sequence::AbstractVector{<:Integer})
InterdisciplinaryPhysicsTeam marked this conversation as resolved.
Show resolved Hide resolved
# Check whether the degree sequence has only non-negative values
all(degree_sequence .>= 0) ||
throw(ArgumentError("The degree sequence must contain non-negative integers only."))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense, to just call isgraphical here? Or do you think that the overhead is too big then?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, in order to maximize performance and code simplicity, we should completely separate the functionalities of isgraphical and havel_hakimi_graph. So if the user knows that the sequence is graphical and just wants the graph, she/he may use havel_hakimi_graph, while if just/also a check for graphicality is needed, then a call to isgraphical must be made.

Then I'd suggest to either:

  1. Remove all checks from havel_hakimi_graph;
  2. Add a bool kwarg check_graphicality to havel_hakimi_graph, that performs the isgraphical check before executing the algorithm (in which case we may skip the subsequent check in the loop).

Similar reasoning would apply to isdigraphical and kleitman_wang_graph.

# Instantiate an empty simple graph
graph = SimpleGraph(length(degree_sequence))
# Create a (vertex, degree) ordered dictionary
vertices_degrees_dict = OrderedDict(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as it work correctly, we can keep it that way, but we probably could make this function much faster, by using a Vector of tuples instead. The sorting step should also be fairly easy to speed up, as after adding a vertex, the degree order is only slightly not sorted.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We unfortunately don't have much time for this, but it would surely help. Would even be better if the final instantiation of a graph object was optional or put in a wrapper method, so that also other packages in the ecosystem may benefit from the full performance of the method (e.g. MutlilayerGraphs.jl's configuration model-like constructors).

vertex => degree for (vertex, degree) in enumerate(degree_sequence)
)
# Havel-Hakimi algorithm
while (any(values(vertices_degrees_dict) .!= 0))
InterdisciplinaryPhysicsTeam marked this conversation as resolved.
Show resolved Hide resolved
# Sort the new sequence in non-increasing order
vertices_degrees_dict = OrderedDict(
sort(collect(vertices_degrees_dict); by=last, rev=true)
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
vertices_degrees_dict = OrderedDict(
sort(collect(vertices_degrees_dict); by=last, rev=true)
)
sort!(vertices_degrees_dict, byvalues=true, rev=true)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It throws an error https://github.com/JuliaGraphs/Graphs.jl/actions/runs/3919202599/jobs/6700055795.

So we reinstated the allocating line.

# Remove the first vertex and distribute its stabs
max_vertex, max_degree = popfirst!(vertices_degrees_dict)
# Check whether the new sequence has only positive values
all(collect(values(vertices_degrees_dict))[1:max_degree] .> 0) ||
throw(ErrorException("The degree sequence is not graphical."))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would probably more efficient to do this check, when set vertices_degrees_dict[vertex] -= 1.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I inserted this check in the loop. But it may be temporary, depending on what comes out of this comment.

# Connect the node of highest degree to other nodes of highest degree
for vertex in collect(keys(vertices_degrees_dict))[1:max_degree]
InterdisciplinaryPhysicsTeam marked this conversation as resolved.
Show resolved Hide resolved
add_edge!(graph, max_vertex, vertex)
vertices_degrees_dict[vertex] -= 1
end
end
# Return the simple graph
return graph
end

"""
lexicographical_order_ntuple(A::NTuple{N,T}, B::NTuple{M,T}) where {N,T}

The less than (lt) function that implements lexicographical order for `NTuple`s of equal length.

See [Wikipedia](https://en.wikipedia.org/wiki/Lexicographic_order).
"""
function lexicographical_order_ntuple(A::Tuple{Vararg{<:Real}}, B::Tuple{Vararg{<:Real}})
length(A) == length(B) ||
throw(ArgumentError("The length of A must match the length of B"))
for (a, b) in zip(A, B)
if a != b
return a < b
end
end

return false
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this function? Julia already implements lexicographical orders for tuples (but it also does that for tuples of different lengths. E.g.

julia> (1, 2) < (3, 4)
true

julia> (1, 2) < (1, 2)
false

julia> (1, 2) < (1, 2, 1)
true

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, we didn't think this functionality was implemented out of the box. We then removed lexicographical_order_ntuple.


"""
kleitman_wang_graph_generator(indegree_sequence::AbstractVector{<:Integer},outdegree_sequence::AbstractVector{<:Integer})

Returns a simple directed graph with given finite in-degree and out-degree sequences of non-negative integers generated via the Kleitman-Wang algorithm, that works like follows:
1. Sort the indegree-outdegree pairs in lexicographical order;
2. Select a pair that has strictly positive outdegree, say the i-th pairs that has outdegree = b_i;
3. Subtract 1 to the first b_i highest indegrees (the i-th being excluded), and set b_i to 0;
4. Repeat from 1. until all indegree-outdegree pairs are of the form (0.0).

## References
- [Wikipedia](https://en.wikipedia.org/wiki/Kleitman%E2%80%93Wang_algorithms)
- [Kleitman and Wang (1973)](https://doi.org/10.1016/0012-365X(73)90037-X)
"""
function kleitman_wang_graph_generator(
indegree_sequence::AbstractVector{<:Integer},
outdegree_sequence::AbstractVector{<:Integer},
)
length(indegree_sequence) == length(outdegree_sequence) || throw(
ArgumentError(
"The provided `indegree_sequence` and `outdegree_sequence` must be of the dame length.",
),
)
# Check whether the indegree_sequence and outdegree_sequence have only non-negative values
all(indegree_sequence .>= 0) || throw(
ArgumentError(
"The `indegree_sequence` sequence must contain non-negative integers only."
),
)
all(outdegree_sequence .>= 0) || throw(
ArgumentError(
"The `outdegree_sequence` sequence must contain non-negative integers only."
),
)

# Instantiate an empty simple graph
graph = SimpleDiGraph(length(indegree_sequence))
# Create a (vertex, degree) ordered dictionary
S = zip(deepcopy(indegree_sequence), deepcopy(outdegree_sequence))
InterdisciplinaryPhysicsTeam marked this conversation as resolved.
Show resolved Hide resolved
vertices_degrees_dict = OrderedDict(i => tup for (i, tup) in enumerate(S))
# Kleitman-Wang algorithm
while (any(Iterators.flatten(values(vertices_degrees_dict)) .!= 0))
# Sort the new sequence in non-increasing lexicographical order
vertices_degrees_dict = OrderedDict(
sort(
collect(vertices_degrees_dict);
by=last,
lt=lexicographical_order_ntuple,
rev=true,
),
)
# Find a vertex with positive outdegree,a nd temporarily remove it from `vertices_degrees_dict`
i, (a_i, b_i) = 0, (0, 0)
for (_i, (_a_i, _b_i)) in collect(deepcopy(vertices_degrees_dict))
if _b_i != 0
i, a_i, b_i = (_i, _a_i, _b_i)
delete!(vertices_degrees_dict, _i)
break
end
end
# Connect the vertex found above to other nodes of highest degree
for (v, degs) in collect(vertices_degrees_dict)[1:b_i]
add_edge!(graph, i, v)
vertices_degrees_dict[v] = (degs[1] - 1, degs[2])
end
# Check whether the new sequence has only positive values
all(
collect(Iterators.flatten(collect(values(vertices_degrees_dict))))[1:b_i] .>= 0
) || throw(
ErrorException("The in-degree and out-degree sequences are not digraphical."),
)
# Reinsert the vertex, with zero outdegree
vertices_degrees_dict[i] = (a_i, 0)
end
return graph
end

"""
random_regular_digraph(n, k)

Expand Down
49 changes: 49 additions & 0 deletions test/simplegraphs/generators/randgraphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,55 @@
@test is_directed(rr) == false
end

@testset "havel hakimi" begin
rr = havel_hakimi_graph_generator(repeat([2, 4], 5))
@test nv(rr) == 10
@test ne(rr) == 15
@test is_directed(rr) == false

rr = havel_hakimi_graph_generator(zeros(Int, 1000))
@test nv(rr) == 1000
@test ne(rr) == 0
@test is_directed(rr) == false

rr = havel_hakimi_graph_generator([2, 2, 2])
@test nv(rr) == 3
@test ne(rr) == 3
@test is_directed(rr) == false

graph = SimpleGraph(10, 15)
degree_sequence = degree(graph)
rr = havel_hakimi_graph_generator(degree_sequence)
@test nv(rr) == 10
@test ne(rr) == 15
@test is_directed(rr) == false
end

@testset "kleitman wang" begin
rr = kleitman_wang_graph_generator(repeat([2, 4], 5), repeat([2, 4], 5))
@test nv(rr) == 10
@test ne(rr) == 30
@test is_directed(rr) == true

rr = kleitman_wang_graph_generator(zeros(Int, 1000), zeros(Int, 1000))
@test nv(rr) == 1000
@test ne(rr) == 0
@test is_directed(rr) == true

rr = kleitman_wang_graph_generator([2, 2, 2], [2, 2, 2])
@test nv(rr) == 3
@test ne(rr) == 6
@test is_directed(rr) == true

graph = SimpleDiGraph(10, 15)
indegree_sequence = indegree(graph)
outdegree_sequence = outdegree(graph)
rr = kleitman_wang_graph_generator(indegree_sequence, outdegree_sequence)
@test nv(rr) == 10
@test ne(rr) == 15
@test is_directed(rr) == true
end

@testset "random tournament" begin
rt = random_tournament_digraph(10; rng=rng)
@test nv(rt) == 10
Expand Down