Skip to content

Commit

Permalink
Add G(n,m) random graph generator
Browse files Browse the repository at this point in the history
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 Qiskit#174
  • Loading branch information
MoAllabbad committed Oct 19, 2020
1 parent b92db69 commit 8389787
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_some() {
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_some() {
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 @@ -1769,6 +1920,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_k_shortest_path_lengths))?;
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 8389787

Please sign in to comment.