diff --git a/docs/source/api.rst b/docs/source/api.rst index 5c96e26c4..0abc5fa8a 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -26,6 +26,10 @@ Generators retworkx.generators.directed_path_graph retworkx.generators.star_graph retworkx.generators.directed_star_graph + retworkx.generators.mesh_graph + retworkx.generators.directed_mesh_graph + retworkx.generators.grid_graph + retworkx.generators.directed_grid_graph Random Circuit Functions ------------------------ diff --git a/src/generators.rs b/src/generators.rs index 2d92f2fb8..0944db756 100644 --- a/src/generators.rs +++ b/src/generators.rs @@ -551,6 +551,421 @@ pub fn star_graph( }) } +/// Generate an undirected mesh graph where every node is connected to every other +/// +/// :param int num_node: The number of nodes to generate the graph with. Node +/// weights will be None if this is specified. If both ``num_node`` and +/// ``weights`` are set this will be ignored and ``weights`` will be used. +/// :param list weights: A list of node weights. If both ``num_node`` and +/// ``weights`` are set this will be ignored and ``weights`` will be used. +/// +/// :returns: The generated mesh graph +/// :rtype: PyGraph +/// :raises IndexError: If neither ``num_nodes`` or ``weights`` are specified +/// +/// .. jupyter-execute:: +/// +/// import os +/// import tempfile +/// +/// import pydot +/// from PIL import Image +/// +/// import retworkx.generators +/// +/// graph = retworkx.generators.mesh_graph(4) +/// dot_str = graph.to_dot( +/// lambda node: dict( +/// color='black', fillcolor='lightblue', style='filled')) +/// dot = pydot.graph_from_dot_data(dot_str)[0] +/// +/// with tempfile.TemporaryDirectory() as tmpdirname: +/// tmp_path = os.path.join(tmpdirname, 'dag.png') +/// dot.write_png(tmp_path) +/// image = Image.open(tmp_path) +/// os.remove(tmp_path) +/// image +/// +#[pyfunction] +#[text_signature = "(/, num_nodes=None, weights=None)"] +pub fn mesh_graph( + py: Python, + num_nodes: Option, + weights: Option>, +) -> PyResult { + let mut graph = StableUnGraph::::default(); + if weights.is_none() && num_nodes.is_none() { + return Err(PyIndexError::new_err( + "num_nodes and weights list not specified", + )); + } + let nodes: Vec = match weights { + Some(weights) => { + let mut node_list: Vec = Vec::new(); + for weight in weights { + let index = graph.add_node(weight); + node_list.push(index); + } + node_list + } + None => (0..num_nodes.unwrap()) + .map(|_| graph.add_node(py.None())) + .collect(), + }; + + let nodelen = nodes.len(); + for i in 0..nodelen - 1 { + for j in i + 1..nodelen { + graph.add_edge(nodes[i], nodes[j], py.None()); + } + } + Ok(graph::PyGraph { + graph, + node_removed: false, + }) +} + +/// Generate a directed mesh graph where every node is connected to every other +/// +/// :param int num_node: The number of nodes to generate the graph with. Node +/// weights will be None if this is specified. If both ``num_node`` and +/// ``weights`` are set this will be ignored and ``weights`` will be used. +/// :param list weights: A list of node weights. If both ``num_node`` and +/// ``weights`` are set this will be ignored and ``weights`` will be used. +/// +/// :returns: The generated mesh graph +/// :rtype: PyDiGraph +/// :raises IndexError: If neither ``num_nodes`` or ``weights`` are specified +/// +/// .. jupyter-execute:: +/// +/// import os +/// import tempfile +/// +/// import pydot +/// from PIL import Image +/// +/// import retworkx.generators +/// +/// graph = retworkx.generators.directed_mesh_graph(4) +/// dot_str = graph.to_dot( +/// lambda node: dict( +/// color='black', fillcolor='lightblue', style='filled')) +/// dot = pydot.graph_from_dot_data(dot_str)[0] +/// +/// with tempfile.TemporaryDirectory() as tmpdirname: +/// tmp_path = os.path.join(tmpdirname, 'dag.png') +/// dot.write_png(tmp_path) +/// image = Image.open(tmp_path) +/// os.remove(tmp_path) +/// image +/// +#[pyfunction] +#[text_signature = "(/, num_nodes=None, weights=None)"] +pub fn directed_mesh_graph( + py: Python, + num_nodes: Option, + weights: Option>, +) -> PyResult { + let mut graph = StableDiGraph::::default(); + if weights.is_none() && num_nodes.is_none() { + return Err(PyIndexError::new_err( + "num_nodes and weights list not specified", + )); + } + let nodes: Vec = match weights { + Some(weights) => { + let mut node_list: Vec = Vec::new(); + for weight in weights { + let index = graph.add_node(weight); + node_list.push(index); + } + node_list + } + None => (0..num_nodes.unwrap()) + .map(|_| graph.add_node(py.None())) + .collect(), + }; + let nodelen = nodes.len(); + for i in 0..nodelen - 1 { + for j in i + 1..nodelen { + graph.add_edge(nodes[i], nodes[j], py.None()); + graph.add_edge(nodes[j], nodes[i], py.None()); + } + } + Ok(digraph::PyDiGraph { + graph, + node_removed: false, + check_cycle: false, + cycle_state: algo::DfsSpace::default(), + }) +} + +/// Generate an undirected grid graph. +/// +/// :param int rows: The number of rows to generate the graph with. +/// If specified, cols also need to be specified +/// :param list cols: The number of rows to generate the graph with. +/// If specified, rows also need to be specified. rows*cols +/// defines the number of nodes in the graph +/// :param list weights: A list of node weights. Nodes are filled row wise. +/// If rows and cols are not specified, then a linear graph containing +/// all the values in weights list is created. +/// If number of nodes(rows*cols) is less than length of +/// weights list, the trailing weights are ignored. +/// If number of nodes(rows*cols) is greater than length of +/// weights list, extra nodes with None weight are appended. +/// +/// :returns: The generated grid graph +/// :rtype: PyGraph +/// :raises IndexError: If neither ``rows`` or ``cols`` and ``weights`` are +/// specified +/// +/// .. jupyter-execute:: +/// +/// import os +/// import tempfile +/// +/// import pydot +/// from PIL import Image +/// +/// import retworkx.generators +/// +/// graph = retworkx.generators.grid_graph(2, 3) +/// dot_str = graph.to_dot( +/// lambda node: dict( +/// color='black', fillcolor='lightblue', style='filled')) +/// dot = pydot.graph_from_dot_data(dot_str)[0] +/// +/// with tempfile.TemporaryDirectory() as tmpdirname: +/// tmp_path = os.path.join(tmpdirname, 'dag.png') +/// dot.write_png(tmp_path) +/// image = Image.open(tmp_path) +/// os.remove(tmp_path) +/// image +/// +#[pyfunction] +#[text_signature = "(/, rows=None, cols=None, weights=None)"] +pub fn grid_graph( + py: Python, + rows: Option, + cols: Option, + weights: Option>, +) -> PyResult { + let mut graph = StableUnGraph::::default(); + if weights.is_none() && (rows.is_none() || cols.is_none()) { + return Err(PyIndexError::new_err( + "dimensions and weights list not specified", + )); + } + + let mut rowlen = match rows { + Some(rows) => rows, + None => 0, + }; + + let mut collen = match cols { + Some(cols) => cols, + None => 0, + }; + + let mut num_nodes = rowlen * collen; + + let nodes: Vec = match weights { + Some(weights) => { + let mut node_list: Vec = Vec::new(); + if num_nodes < weights.len() && rowlen == 0 { + collen = weights.len(); + rowlen = 1; + num_nodes = collen; + } + + let mut node_cnt = num_nodes; + + for weight in weights { + if node_cnt == 0 { + break; + } + let index = graph.add_node(weight); + node_list.push(index); + node_cnt -= 1; + } + for _i in 0..node_cnt { + let index = graph.add_node(py.None()); + node_list.push(index); + } + node_list + } + None => (0..num_nodes).map(|_| graph.add_node(py.None())).collect(), + }; + + for i in 0..rowlen { + for j in 0..collen { + if i + 1 < rowlen { + graph.add_edge( + nodes[i * collen + j], + nodes[(i + 1) * collen + j], + py.None(), + ); + } + if j + 1 < collen { + graph.add_edge( + nodes[i * collen + j], + nodes[i * collen + j + 1], + py.None(), + ); + } + } + } + Ok(graph::PyGraph { + graph, + node_removed: false, + }) +} + +/// Generate a directed grid graph. The edges propagate towards right and +/// bottom direction if ``bidirectional`` is ``false`` +/// +/// :param int rows: The number of rows to generate the graph with. +/// If specified, cols also need to be specified. +/// :param list cols: The number of rows to generate the graph with. +/// If specified, rows also need to be specified. rows*cols +/// defines the number of nodes in the graph. +/// :param list weights: A list of node weights. Nodes are filled row wise. +/// If rows and cols are not specified, then a linear graph containing +/// all the values in weights list is created. +/// If number of nodes(rows*cols) is less than length of +/// weights list, the trailing weights are ignored. +/// If number of nodes(rows*cols) is greater than length of +/// weights list, extra nodes with None weight are appended. +/// :param bidirectional: A parameter to indicate if edges should exist in +/// both directions between nodes +/// +/// :returns: The generated grid graph +/// :rtype: PyDiGraph +/// :raises IndexError: If neither ``rows`` or ``cols`` and ``weights`` are +/// specified +/// +/// .. jupyter-execute:: +/// +/// import os +/// import tempfile +/// +/// import pydot +/// from PIL import Image +/// +/// import retworkx.generators +/// +/// graph = retworkx.generators.directed_grid_graph(2, 3) +/// dot_str = graph.to_dot( +/// lambda node: dict( +/// color='black', fillcolor='lightblue', style='filled')) +/// dot = pydot.graph_from_dot_data(dot_str)[0] +/// +/// with tempfile.TemporaryDirectory() as tmpdirname: +/// tmp_path = os.path.join(tmpdirname, 'dag.png') +/// dot.write_png(tmp_path) +/// image = Image.open(tmp_path) +/// os.remove(tmp_path) +/// image +/// +#[pyfunction(bidirectional = "false")] +#[text_signature = "(/, rows=None, cols=None, weights=None, bidirectional=False)"] +pub fn directed_grid_graph( + py: Python, + rows: Option, + cols: Option, + weights: Option>, + bidirectional: bool, +) -> PyResult { + let mut graph = StableDiGraph::::default(); + if weights.is_none() && (rows.is_none() || cols.is_none()) { + return Err(PyIndexError::new_err( + "dimensions and weights list not specified", + )); + } + + let mut rowlen = match rows { + Some(rows) => rows, + None => 0, + }; + + let mut collen = match cols { + Some(cols) => cols, + None => 0, + }; + + let mut num_nodes = rowlen * collen; + + let nodes: Vec = match weights { + Some(weights) => { + let mut node_list: Vec = Vec::new(); + if num_nodes < weights.len() && rowlen == 0 { + collen = weights.len(); + rowlen = 1; + num_nodes = collen; + } + + let mut node_cnt = num_nodes; + + for weight in weights { + if node_cnt == 0 { + break; + } + let index = graph.add_node(weight); + node_list.push(index); + node_cnt -= 1; + } + for _i in 0..node_cnt { + let index = graph.add_node(py.None()); + node_list.push(index); + } + node_list + } + None => (0..num_nodes).map(|_| graph.add_node(py.None())).collect(), + }; + + for i in 0..rowlen { + for j in 0..collen { + if i + 1 < rowlen { + graph.add_edge( + nodes[i * collen + j], + nodes[(i + 1) * collen + j], + py.None(), + ); + if bidirectional { + graph.add_edge( + nodes[(i + 1) * collen + j], + nodes[i * collen + j], + py.None(), + ); + } + } + + if j + 1 < collen { + graph.add_edge( + nodes[i * collen + j], + nodes[i * collen + j + 1], + py.None(), + ); + if bidirectional { + graph.add_edge( + nodes[i * collen + j + 1], + nodes[i * collen + j], + py.None(), + ); + } + } + } + } + Ok(digraph::PyDiGraph { + graph, + node_removed: false, + check_cycle: false, + cycle_state: algo::DfsSpace::default(), + }) +} + #[pymodule] pub fn generators(_py: Python, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(cycle_graph))?; @@ -559,5 +974,9 @@ pub fn generators(_py: Python, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(directed_path_graph))?; m.add_wrapped(wrap_pyfunction!(star_graph))?; m.add_wrapped(wrap_pyfunction!(directed_star_graph))?; + m.add_wrapped(wrap_pyfunction!(mesh_graph))?; + m.add_wrapped(wrap_pyfunction!(directed_mesh_graph))?; + m.add_wrapped(wrap_pyfunction!(grid_graph))?; + m.add_wrapped(wrap_pyfunction!(directed_grid_graph))?; Ok(()) } diff --git a/tests/generators/test_grid.py b/tests/generators/test_grid.py new file mode 100644 index 000000000..02eb59306 --- /dev/null +++ b/tests/generators/test_grid.py @@ -0,0 +1,136 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import unittest + +import retworkx + + +class TestGridGraph(unittest.TestCase): + + def test_directed_grid_graph_dimensions(self): + graph = retworkx.generators.directed_grid_graph(4, 5) + self.assertEqual(len(graph), 20) + self.assertEqual(len(graph.edges()), 31) + self.assertEqual(graph.out_edges(0), [(0, 1, None), (0, 5, None)]) + self.assertEqual(graph.out_edges(7), [(7, 8, None), (7, 12, None)]) + self.assertEqual(graph.out_edges(9), [(9, 14, None)]) + self.assertEqual(graph.out_edges(17), [(17, 18, None)]) + self.assertEqual(graph.out_edges(19), []) + self.assertEqual(graph.in_edges(0), []) + self.assertEqual(graph.in_edges(2), [(1, 2, None)]) + self.assertEqual(graph.in_edges(5), [(0, 5, None)]) + self.assertEqual(graph.in_edges(7), [(6, 7, None), (2, 7, None)]) + self.assertEqual(graph.in_edges(19), [(18, 19, None), (14, 19, None)]) + + def test_directed_grid_graph_weights(self): + graph = retworkx.generators.directed_grid_graph( + weights=list(range(20))) + self.assertEqual(len(graph), 20) + self.assertEqual([x for x in range(20)], graph.nodes()) + self.assertEqual(len(graph.edges()), 19) + for i in range(19): + self.assertEqual(graph.out_edges(i), [(i, i + 1, None)]) + self.assertEqual(graph.out_edges(19), []) + for i in range(1, 20): + self.assertEqual(graph.in_edges(i), [(i - 1, i, None)]) + self.assertEqual(graph.in_edges(0), []) + + def test_directed_grid_graph_dimensions_weights(self): + graph = retworkx.generators.directed_grid_graph( + 4, 5, weights=list(range(20))) + self.assertEqual(len(graph), 20) + self.assertEqual([x for x in range(20)], graph.nodes()) + self.assertEqual(len(graph.edges()), 31) + self.assertEqual(graph.out_edges(0), [(0, 1, None), (0, 5, None)]) + self.assertEqual(graph.out_edges(7), [(7, 8, None), (7, 12, None)]) + self.assertEqual(graph.out_edges(9), [(9, 14, None)]) + self.assertEqual(graph.out_edges(17), [(17, 18, None)]) + self.assertEqual(graph.out_edges(19), []) + self.assertEqual(graph.in_edges(0), []) + self.assertEqual(graph.in_edges(2), [(1, 2, None)]) + self.assertEqual(graph.in_edges(5), [(0, 5, None)]) + self.assertEqual(graph.in_edges(7), [(6, 7, None), (2, 7, None)]) + self.assertEqual(graph.in_edges(19), [(18, 19, None), (14, 19, None)]) + + def test_directed_grid_graph_more_dimensions_weights(self): + graph = retworkx.generators.directed_grid_graph( + 4, 5, weights=list(range(16))) + self.assertEqual(len(graph), 20) + self.assertEqual([x for x in range(16)] + [None] * 4, graph.nodes()) + self.assertEqual(len(graph.edges()), 31) + self.assertEqual(graph.out_edges(0), [(0, 1, None), (0, 5, None)]) + self.assertEqual(graph.out_edges(7), [(7, 8, None), (7, 12, None)]) + self.assertEqual(graph.out_edges(9), [(9, 14, None)]) + self.assertEqual(graph.out_edges(17), [(17, 18, None)]) + self.assertEqual(graph.out_edges(19), []) + self.assertEqual(graph.in_edges(0), []) + self.assertEqual(graph.in_edges(2), [(1, 2, None)]) + self.assertEqual(graph.in_edges(5), [(0, 5, None)]) + self.assertEqual(graph.in_edges(7), [(6, 7, None), (2, 7, None)]) + self.assertEqual(graph.in_edges(19), [(18, 19, None), (14, 19, None)]) + + def test_directed_grid_graph_less_dimensions_weights(self): + graph = retworkx.generators.directed_grid_graph( + 4, 5, weights=list(range(24))) + self.assertEqual(len(graph), 20) + self.assertEqual([x for x in range(20)], graph.nodes()) + self.assertEqual(len(graph.edges()), 31) + self.assertEqual(graph.out_edges(0), [(0, 1, None), (0, 5, None)]) + self.assertEqual(graph.out_edges(7), [(7, 8, None), (7, 12, None)]) + self.assertEqual(graph.out_edges(9), [(9, 14, None)]) + self.assertEqual(graph.out_edges(17), [(17, 18, None)]) + self.assertEqual(graph.out_edges(19), []) + self.assertEqual(graph.in_edges(0), []) + self.assertEqual(graph.in_edges(2), [(1, 2, None)]) + self.assertEqual(graph.in_edges(5), [(0, 5, None)]) + self.assertEqual(graph.in_edges(7), [(6, 7, None), (2, 7, None)]) + self.assertEqual(graph.in_edges(19), [(18, 19, None), (14, 19, None)]) + + def test_grid_directed_no_weights_or_dim(self): + with self.assertRaises(IndexError): + retworkx.generators.directed_grid_graph() + retworkx.generators.directed_grid_graph(rows=5, weights=[1] * 5) + retworkx.generators.directed_grid_graph(cols=5, weights=[1] * 5) + + def test_grid_graph_dimensions(self): + graph = retworkx.generators.grid_graph(4, 5) + self.assertEqual(len(graph), 20) + self.assertEqual(len(graph.edges()), 31) + + def test_grid_graph_weights(self): + graph = retworkx.generators.grid_graph(weights=list(range(20))) + self.assertEqual(len(graph), 20) + self.assertEqual([x for x in range(20)], graph.nodes()) + self.assertEqual(len(graph.edges()), 19) + + def test_grid_graph_dimensions_weights(self): + graph = retworkx.generators.grid_graph(4, 5, weights=list(range(20))) + self.assertEqual(len(graph), 20) + self.assertEqual([x for x in range(20)], graph.nodes()) + self.assertEqual(len(graph.edges()), 31) + + graph = retworkx.generators.grid_graph(4, 5, weights=list(range(16))) + self.assertEqual(len(graph), 20) + self.assertEqual([x for x in range(16)] + [None] * 4, graph.nodes()) + self.assertEqual(len(graph.edges()), 31) + + graph = retworkx.generators.grid_graph(4, 5, weights=list(range(24))) + self.assertEqual(len(graph), 20) + self.assertEqual([x for x in range(20)], graph.nodes()) + self.assertEqual(len(graph.edges()), 31) + + def test_grid_no_weights_or_dim(self): + with self.assertRaises(IndexError): + retworkx.generators.grid_graph() + retworkx.generators.grid_graph(rows=5, weights=[1] * 5) + retworkx.generators.grid_graph(cols=5, weights=[1] * 5) diff --git a/tests/generators/test_mesh.py b/tests/generators/test_mesh.py new file mode 100644 index 000000000..dabd2463b --- /dev/null +++ b/tests/generators/test_mesh.py @@ -0,0 +1,61 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import unittest + +import retworkx + + +class TestMeshGraph(unittest.TestCase): + + def test_directed_mesh_graph(self): + graph = retworkx.generators.directed_mesh_graph(20) + self.assertEqual(len(graph), 20) + self.assertEqual(len(graph.edges()), 380) + for i in range(20): + ls = [] + for j in range(19, -1, -1): + if i != j: + ls.append((i, j, None)) + self.assertEqual(graph.out_edges(i), ls) + + def test_directed_mesh_graph_weights(self): + graph = retworkx.generators.directed_mesh_graph( + weights=list(range(20))) + self.assertEqual(len(graph), 20) + self.assertEqual([x for x in range(20)], graph.nodes()) + self.assertEqual(len(graph.edges()), 380) + for i in range(20): + ls = [] + for j in range(19, -1, -1): + if i != j: + ls.append((i, j, None)) + self.assertEqual(graph.out_edges(i), ls) + + def test_mesh_directed_no_weights_or_num(self): + with self.assertRaises(IndexError): + retworkx.generators.directed_mesh_graph() + + def test_mesh_graph(self): + graph = retworkx.generators.mesh_graph(20) + self.assertEqual(len(graph), 20) + self.assertEqual(len(graph.edges()), 190) + + def test_mesh_graph_weights(self): + graph = retworkx.generators.mesh_graph(weights=list(range(20))) + self.assertEqual(len(graph), 20) + self.assertEqual([x for x in range(20)], graph.nodes()) + self.assertEqual(len(graph.edges()), 190) + + def test_mesh_no_weights_or_num(self): + with self.assertRaises(IndexError): + retworkx.generators.mesh_graph()