Skip to content

Commit

Permalink
Add G(n,m) random graph generator (#176)
Browse files Browse the repository at this point in the history
* Add G(n,m) random graph generator

This adds two G(n,m) graph generators, one directed and one undirected.
The generated graph will be a random graph out of all the possible
graphs that are n nodes and m edges with max n*(n-1) edges for directed
graphs and n*(n-1)/2 for undirected graphs, avoiding self-loops and
multigraphs. The run time is O(n+m)
Fixes #174

* Change !.is_some() to is_none()

Apply suggestions from code review
  • Loading branch information
MoAllabbad committed Oct 19, 2020
1 parent fff0fae commit 9c4ecf5
Show file tree
Hide file tree
Showing 3 changed files with 242 additions and 0 deletions.
2 changes: 2 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Random Circuit Functions

retworkx.directed_gnp_random_graph
retworkx.undirected_gnp_random_graph
retworkx.directed_gnm_random_graph
retworkx.undirected_gnm_random_graph

Algorithm Functions
-------------------
Expand Down
153 changes: 153 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1607,6 +1607,157 @@ pub fn undirected_gnp_random_graph(
Ok(graph)
}

/// Return a :math:`G_{nm}` of a directed graph
///
/// Generates a random directed graph out of all the possible graphs with :math:`n` nodes and
/// :math:`m` edges. The generated graph will not be a multigraph and will not have self loops.
///
/// For :math:`n` nodes, the maximum edges that can be returned is :math:`n (n - 1)`.
/// Passing :math:`m` higher than that will still return the maximum number of edges.
/// If :math:`m = 0`, the returned graph will always be empty (no edges).
/// When a seed is provided, the results are reproducible. Passing a seed when :math:`m = 0`
/// or :math:`m >= n (n - 1)` has no effect, as the result will always be an empty or a complete graph respectively.
///
/// This algorithm has a time complexity of :math:`O(n + m)`
///
/// :param int num_nodes: The number of nodes to create in the graph
/// :param int num_edges: The number of edges to create in the graph
/// :param int seed: An optional seed to use for the random number generator
///
/// :return: A PyDiGraph object
/// :rtype: PyDiGraph
///
#[pyfunction]
#[text_signature = "(num_nodes, num_edges, seed=None, /)"]
pub fn directed_gnm_random_graph(
py: Python,
num_nodes: isize,
num_edges: isize,
seed: Option<u64>,
) -> PyResult<digraph::PyDiGraph> {
if num_nodes <= 0 {
return Err(PyValueError::new_err("num_nodes must be > 0"));
}
if num_edges < 0 {
return Err(PyValueError::new_err("num_edges must be >= 0"));
}
let mut rng: Pcg64 = match seed {
Some(seed) => Pcg64::seed_from_u64(seed),
None => Pcg64::from_entropy(),
};
let mut inner_graph = StableDiGraph::<PyObject, PyObject>::new();
for x in 0..num_nodes {
inner_graph.add_node(x.to_object(py));
}
// if number of edges to be created is >= max,
// avoid randomly missed trials and directly add edges between every node
if num_edges >= num_nodes * (num_nodes - 1) {
for u in 0..num_nodes {
for v in 0..num_nodes {
// avoid self-loops
if u != v {
let u_index = NodeIndex::new(u as usize);
let v_index = NodeIndex::new(v as usize);
inner_graph.add_edge(u_index, v_index, py.None());
}
}
}
} else {
let mut created_edges: isize = 0;
while created_edges < num_edges {
let u = rng.gen_range(0, num_nodes);
let v = rng.gen_range(0, num_nodes);
let u_index = NodeIndex::new(u as usize);
let v_index = NodeIndex::new(v as usize);
// avoid self-loops and multi-graphs
if u != v && inner_graph.find_edge(u_index, v_index).is_none() {
inner_graph.add_edge(u_index, v_index, py.None());
created_edges += 1;
}
}
}
let graph = digraph::PyDiGraph {
graph: inner_graph,
cycle_state: algo::DfsSpace::default(),
check_cycle: false,
node_removed: false,
};
Ok(graph)
}

/// Return a :math:`G_{nm}` of an undirected graph
///
/// Generates a random undirected graph out of all the possible graphs with :math:`n` nodes and
/// :math:`m` edges. The generated graph will not be a multigraph and will not have self loops.
///
/// For :math:`n` nodes, the maximum edges that can be returned is :math:`n (n - 1)/2`.
/// Passing :math:`m` higher than that will still return the maximum number of edges.
/// If :math:`m = 0`, the returned graph will always be empty (no edges).
/// When a seed is provided, the results are reproducible. Passing a seed when :math:`m = 0`
/// or :math:`m >= n (n - 1)/2` has no effect, as the result will always be an empty or a complete graph respectively.
///
/// This algorithm has a time complexity of :math:`O(n + m)`
///
/// :param int num_nodes: The number of nodes to create in the graph
/// :param int num_edges: The number of edges to create in the graph
/// :param int seed: An optional seed to use for the random number generator
///
/// :return: A PyGraph object
/// :rtype: PyGraph

#[pyfunction]
#[text_signature = "(num_nodes, probability, seed=None, /)"]
pub fn undirected_gnm_random_graph(
py: Python,
num_nodes: isize,
num_edges: isize,
seed: Option<u64>,
) -> PyResult<graph::PyGraph> {
if num_nodes <= 0 {
return Err(PyValueError::new_err("num_nodes must be > 0"));
}
if num_edges < 0 {
return Err(PyValueError::new_err("num_edges must be >= 0"));
}
let mut rng: Pcg64 = match seed {
Some(seed) => Pcg64::seed_from_u64(seed),
None => Pcg64::from_entropy(),
};
let mut inner_graph = StableUnGraph::<PyObject, PyObject>::default();
for x in 0..num_nodes {
inner_graph.add_node(x.to_object(py));
}
// if number of edges to be created is >= max,
// avoid randomly missed trials and directly add edges between every node
if num_edges >= num_nodes * (num_nodes - 1) / 2 {
for u in 0..num_nodes {
for v in u + 1..num_nodes {
let u_index = NodeIndex::new(u as usize);
let v_index = NodeIndex::new(v as usize);
inner_graph.add_edge(u_index, v_index, py.None());
}
}
} else {
let mut created_edges: isize = 0;
while created_edges < num_edges {
let u = rng.gen_range(0, num_nodes);
let v = rng.gen_range(0, num_nodes);
let u_index = NodeIndex::new(u as usize);
let v_index = NodeIndex::new(v as usize);
// avoid self-loops and multi-graphs
if u != v && inner_graph.find_edge(u_index, v_index).is_none() {
inner_graph.add_edge(u_index, v_index, py.None());
created_edges += 1;
}
}
}
let graph = graph::PyGraph {
graph: inner_graph,
node_removed: false,
};
Ok(graph)
}

/// Return a list of cycles which form a basis for cycles of a given PyGraph
///
/// A basis for cycles of a graph is a minimal collection of
Expand Down Expand Up @@ -1850,6 +2001,8 @@ fn retworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(graph_greedy_color))?;
m.add_wrapped(wrap_pyfunction!(directed_gnp_random_graph))?;
m.add_wrapped(wrap_pyfunction!(undirected_gnp_random_graph))?;
m.add_wrapped(wrap_pyfunction!(directed_gnm_random_graph))?;
m.add_wrapped(wrap_pyfunction!(undirected_gnm_random_graph))?;
m.add_wrapped(wrap_pyfunction!(cycle_basis))?;
m.add_wrapped(wrap_pyfunction!(strongly_connected_components))?;
m.add_wrapped(wrap_pyfunction!(digraph_find_cycle))?;
Expand Down
87 changes: 87 additions & 0 deletions tests/test_random.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,90 @@ def test_random_gnp_undirected_invalid_num_nodes(self):
def test_random_gnp_undirected_invalid_probability(self):
with self.assertRaises(ValueError):
retworkx.undirected_gnp_random_graph(23, 123.5)


class TestGNMRandomGraph(unittest.TestCase):

def test_random_gnm_directed(self):
graph = retworkx.directed_gnm_random_graph(20, 100)
self.assertEqual(len(graph), 20)
self.assertEqual(len(graph.edges()), 100)
# with other arguments equal, same seed results in same graph
graph_s1 = retworkx.directed_gnm_random_graph(20, 100, seed=10)
graph_s2 = retworkx.directed_gnm_random_graph(20, 100, seed=10)
self.assertEqual(graph_s1.edge_list(), graph_s2.edge_list())

def test_random_gnm_directed_empty_graph(self):
graph = retworkx.directed_gnm_random_graph(20, 0)
self.assertEqual(len(graph), 20)
self.assertEqual(len(graph.edges()), 0)
# passing a seed when passing zero edges has no effect
graph = retworkx.directed_gnm_random_graph(20, 0, 44)
self.assertEqual(len(graph), 20)
self.assertEqual(len(graph.edges()), 0)

def test_random_gnm_directed_complete_graph(self):
n = 20
max_m = n * (n - 1)
# passing the max edges for the passed number of nodes
graph = retworkx.directed_gnm_random_graph(n, max_m)
self.assertEqual(len(graph), n)
self.assertEqual(len(graph.edges()), max_m)
# passing m > the max edges n(n-1) still returns the max edges
graph = retworkx.directed_gnm_random_graph(n, max_m + 1)
self.assertEqual(len(graph), n)
self.assertEqual(len(graph.edges()), max_m)
# passing a seed when passing max edges has no effect
graph = retworkx.directed_gnm_random_graph(n, max_m, 55)
self.assertEqual(len(graph), n)
self.assertEqual(len(graph.edges()), max_m)

def test_random_gnm_directed_invalid_num_nodes(self):
with self.assertRaises(ValueError):
retworkx.directed_gnm_random_graph(-23, 5)

def test_random_gnm_directed_invalid_num_edges(self):
with self.assertRaises(ValueError):
retworkx.directed_gnm_random_graph(23, -5)

def test_random_gnm_undirected(self):
graph = retworkx.undirected_gnm_random_graph(20, 100)
self.assertEqual(len(graph), 20)
self.assertEqual(len(graph.edges()), 100)
# with other arguments equal, same seed results in same graph
graph_s1 = retworkx.undirected_gnm_random_graph(20, 100, seed=10)
graph_s2 = retworkx.undirected_gnm_random_graph(20, 100, seed=10)
self.assertEqual(graph_s1.edge_list(), graph_s2.edge_list())

def test_random_gnm_undirected_empty_graph(self):
graph = retworkx.undirected_gnm_random_graph(20, 0)
self.assertEqual(len(graph), 20)
self.assertEqual(len(graph.edges()), 0)
# passing a seed when passing zero edges has no effect
graph = retworkx.undirected_gnm_random_graph(20, 0, 44)
self.assertEqual(len(graph), 20)
self.assertEqual(len(graph.edges()), 0)

def test_random_gnm_undirected_complete_graph(self):
n = 20
max_m = n * (n - 1) // 2
# passing the max edges for the passed number of nodes
graph = retworkx.undirected_gnm_random_graph(n, max_m)
self.assertEqual(len(graph), n)
self.assertEqual(len(graph.edges()), max_m)
# passing m > the max edges n(n-1)/2 still returns the max edges
graph = retworkx.undirected_gnm_random_graph(n, max_m + 1)
self.assertEqual(len(graph), n)
self.assertEqual(len(graph.edges()), max_m)
# passing a seed when passing max edges has no effect
graph = retworkx.undirected_gnm_random_graph(n, max_m, 55)
self.assertEqual(len(graph), n)
self.assertEqual(len(graph.edges()), max_m)

def test_random_gnm_undirected_invalid_num_nodes(self):
with self.assertRaises(ValueError):
retworkx.undirected_gnm_random_graph(-23, 5)

def test_random_gnm_undirected_invalid_probability(self):
with self.assertRaises(ValueError):
retworkx.undirected_gnm_random_graph(23, -5)

0 comments on commit 9c4ecf5

Please sign in to comment.