diff --git a/docs/source/api.rst b/docs/source/api.rst index 27dea60445..6d05ceff85 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -14,6 +14,19 @@ Graph Classes retworkx.PyDiGraph retworkx.PyDAG +Generators +---------- + +.. autosummary:: + :toctree: stubs + + retworkx.generators.cycle_graph + retworkx.generators.directed_cycle_graph + retworkx.generators.path_graph + retworkx.generators.directed_path_graph + retworkx.generators.star_graph + retworkx.generators.directed_star_graph + Random Circuit Functions ------------------------ diff --git a/docs/source/index.rst b/docs/source/index.rst index c333214ab3..970a809eae 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -5,7 +5,7 @@ retworkx Documentation Contents: .. toctree:: - :maxdepth: 2 + :maxdepth: 3 README Retworkx API diff --git a/retworkx/__init__.py b/retworkx/__init__.py index 0c60929eba..87d404c799 100644 --- a/retworkx/__init__.py +++ b/retworkx/__init__.py @@ -6,7 +6,10 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. +import sys + from .retworkx import * +sys.modules['retworkx.generators'] = generators class PyDAG(PyDiGraph): diff --git a/src/generators.rs b/src/generators.rs new file mode 100644 index 0000000000..0598023e3b --- /dev/null +++ b/src/generators.rs @@ -0,0 +1,534 @@ +// 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. + +use std::iter; + +use petgraph::algo; +use petgraph::graph::NodeIndex; +use petgraph::stable_graph::{StableDiGraph, StableUnGraph}; + +use pyo3::exceptions::IndexError; +use pyo3::prelude::*; +use pyo3::wrap_pyfunction; +use pyo3::Python; + +use super::digraph; +use super::graph; + +fn pairwise(right: I) -> impl Iterator, I::Item)> +where + I: IntoIterator + Clone, +{ + let left = iter::once(None).chain(right.clone().into_iter().map(Some)); + left.zip(right) +} + +/// Generate a cycle graph +/// +/// :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, the first element in the list +/// will be the center node of the cycle graph. If both ``num_node`` and +/// ``weights`` are set this will be ignored and ``weights`` will be used. +/// +/// :returns: The generated cycle 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_cycle_graph(5) +/// 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_cycle_graph( + py: Python, + num_nodes: Option, + weights: Option>, +) -> PyResult { + let mut graph = StableDiGraph::::default(); + if weights.is_none() && num_nodes.is_none() { + return Err(IndexError::py_err( + "num_nodes and weights list not specified", + )); + } + let node_len: usize; + let nodes: Vec = match weights { + Some(weights) => { + let mut node_list: Vec = Vec::new(); + node_len = weights.len(); + for weight in weights { + let index = graph.add_node(weight); + node_list.push(index); + } + node_list + } + None => { + node_len = num_nodes.unwrap(); + (0..num_nodes.unwrap()) + .map(|_| graph.add_node(py.None())) + .collect() + } + }; + for (node_a, node_b) in pairwise(nodes) { + match node_a { + Some(node_a) => graph.add_edge(node_a, node_b, py.None()), + None => continue, + }; + } + let last_node_index = NodeIndex::new(node_len - 1); + let first_node_index = NodeIndex::new(0); + graph.add_edge(last_node_index, first_node_index, py.None()); + Ok(digraph::PyDiGraph { + graph, + node_removed: false, + check_cycle: false, + cycle_state: algo::DfsSpace::default(), + }) +} + +/// Generate an undirected cycle graph +/// +/// :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, the first element in the list +/// will be the center node of the cycle graph. If both ``num_node`` and +/// ``weights`` are set this will be ignored and ``weights`` will be used. +/// +/// :returns: The generated cycle 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.cycle_graph(5) +/// 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 cycle_graph( + py: Python, + num_nodes: Option, + weights: Option>, +) -> PyResult { + let mut graph = StableUnGraph::::default(); + if weights.is_none() && num_nodes.is_none() { + return Err(IndexError::py_err( + "num_nodes and weights list not specified", + )); + } + let node_len: usize; + let nodes: Vec = match weights { + Some(weights) => { + let mut node_list: Vec = Vec::new(); + node_len = weights.len(); + for weight in weights { + let index = graph.add_node(weight); + node_list.push(index); + } + node_list + } + None => { + node_len = num_nodes.unwrap(); + (0..num_nodes.unwrap()) + .map(|_| graph.add_node(py.None())) + .collect() + } + }; + for (node_a, node_b) in pairwise(nodes) { + match node_a { + Some(node_a) => graph.add_edge(node_a, node_b, py.None()), + None => continue, + }; + } + let last_node_index = NodeIndex::new(node_len - 1); + let first_node_index = NodeIndex::new(0); + graph.add_edge(last_node_index, first_node_index, py.None()); + Ok(graph::PyGraph { + graph, + node_removed: false, + }) +} + +/// Generate a directed path graph +/// +/// :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, the first element in the list +/// will be the center node of the path graph. If both ``num_node`` and +/// ``weights`` are set this will be ignored and ``weights`` will be used. +/// +/// :returns: The generated path 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_path_graph(10) +/// 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_path_graph( + py: Python, + num_nodes: Option, + weights: Option>, +) -> PyResult { + let mut graph = StableDiGraph::::default(); + if weights.is_none() && num_nodes.is_none() { + return Err(IndexError::py_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(), + }; + for (node_a, node_b) in pairwise(nodes) { + match node_a { + Some(node_a) => graph.add_edge(node_a, node_b, py.None()), + None => continue, + }; + } + Ok(digraph::PyDiGraph { + graph, + node_removed: false, + check_cycle: false, + cycle_state: algo::DfsSpace::default(), + }) +} + +/// Generate an undirected path graph +/// +/// :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, the first element in the list +/// will be the center node of the path graph. If both ``num_node`` and +/// ``weights`` are set this will be ignored and ``weights`` will be used. +/// +/// :returns: The generated path 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.path_graph(10) +/// 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 path_graph( + py: Python, + num_nodes: Option, + weights: Option>, +) -> PyResult { + let mut graph = StableUnGraph::::default(); + if weights.is_none() && num_nodes.is_none() { + return Err(IndexError::py_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(), + }; + for (node_a, node_b) in pairwise(nodes) { + match node_a { + Some(node_a) => graph.add_edge(node_a, node_b, py.None()), + None => continue, + }; + } + Ok(graph::PyGraph { + graph, + node_removed: false, + }) +} + +/// Generate a directed star graph +/// +/// :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, the first element in the list +/// will be the center node of the star graph. If both ``num_node`` and +/// ``weights`` are set this will be ignored and ``weights`` will be used. +/// :param bool inward: if set ``True`` the nodes will be directed towards the +/// center node +/// +/// :returns: The generated star 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_star_graph(10) +/// 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 +/// +/// .. jupyter-execute:: +/// +/// import os +/// import tempfile +/// +/// import pydot +/// from PIL import Image +/// +/// import retworkx.generators +/// +/// graph = retworkx.generators.directed_star_graph(10, inward=True) +/// 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(inward = "false")] +#[text_signature = "(/, num_nodes=None, weights=None, inward=False)"] +pub fn directed_star_graph( + py: Python, + num_nodes: Option, + weights: Option>, + inward: bool, +) -> PyResult { + let mut graph = StableDiGraph::::default(); + if weights.is_none() && num_nodes.is_none() { + return Err(IndexError::py_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(), + }; + for node in nodes[1..].iter() { + if inward { + graph.add_edge(*node, nodes[0], py.None()); + } else { + graph.add_edge(nodes[0], *node, py.None()); + } + } + Ok(digraph::PyDiGraph { + graph, + node_removed: false, + check_cycle: false, + cycle_state: algo::DfsSpace::default(), + }) +} + +/// Generate an undirected star graph +/// +/// :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, the first element in the list +/// will be the center node of the star graph. If both ``num_node`` and +/// ``weights`` are set this will be ignored and ``weights`` will be used. +/// +/// :returns: The generated star 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.star_graph(10) +/// 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 star_graph( + py: Python, + num_nodes: Option, + weights: Option>, +) -> PyResult { + let mut graph = StableUnGraph::::default(); + if weights.is_none() && num_nodes.is_none() { + return Err(IndexError::py_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(), + }; + for node in nodes[1..].iter() { + graph.add_edge(nodes[0], *node, py.None()); + } + Ok(graph::PyGraph { + graph, + node_removed: false, + }) +} + +#[pymodule] +pub fn generators(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(cycle_graph))?; + m.add_wrapped(wrap_pyfunction!(directed_cycle_graph))?; + m.add_wrapped(wrap_pyfunction!(path_graph))?; + m.add_wrapped(wrap_pyfunction!(directed_path_graph))?; + m.add_wrapped(wrap_pyfunction!(star_graph))?; + m.add_wrapped(wrap_pyfunction!(directed_star_graph))?; + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index bba5e7ebf2..8bd5deef32 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ mod dag_isomorphism; mod digraph; mod dijkstra; mod dot_utils; +mod generators; mod graph; use std::cmp::{Ordering, Reverse}; @@ -37,6 +38,7 @@ use pyo3::exceptions::{Exception, ValueError}; use pyo3::prelude::*; use pyo3::types::{PyDict, PyList}; use pyo3::wrap_pyfunction; +use pyo3::wrap_pymodule; use pyo3::Python; use petgraph::algo; @@ -50,6 +52,8 @@ use rand::prelude::*; use rand_pcg::Pcg64; use rayon::prelude::*; +use generators::PyInit_generators; + fn longest_path(graph: &digraph::PyDiGraph) -> PyResult> { let dag = &graph.graph; let mut path: Vec = Vec::new(); @@ -1386,6 +1390,7 @@ fn retworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(strongly_connected_components))?; m.add_class::()?; m.add_class::()?; + m.add_wrapped(wrap_pymodule!(generators))?; Ok(()) } diff --git a/tests/generators/__init__.py b/tests/generators/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/generators/test_cycle.py b/tests/generators/test_cycle.py new file mode 100644 index 0000000000..79ee5f0942 --- /dev/null +++ b/tests/generators/test_cycle.py @@ -0,0 +1,55 @@ +# 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 TestCycleGraph(unittest.TestCase): + + def test_directed_cycle_graph(self): + graph = retworkx.generators.directed_cycle_graph(20) + self.assertEqual(len(graph), 20) + self.assertEqual(len(graph.edges()), 20) + for i in range(19): + self.assertEqual(graph.out_edges(i), [(i, i + 1, None)]) + self.assertEqual(graph.out_edges(19), [(19, 0, None)]) + + def test_directed_cycle_graph_weights(self): + graph = retworkx.generators.directed_cycle_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()), 20) + for i in range(19): + self.assertEqual(graph.out_edges(i), [(i, i + 1, None)]) + self.assertEqual(graph.out_edges(19), [(19, 0, None)]) + + def test_cycle_directed_no_weights_or_num(self): + with self.assertRaises(IndexError): + retworkx.generators.directed_cycle_graph() + + def test_cycle_graph(self): + graph = retworkx.generators.cycle_graph(20) + self.assertEqual(len(graph), 20) + self.assertEqual(len(graph.edges()), 20) + + def test_cycle_graph_weights(self): + graph = retworkx.generators.cycle_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()), 20) + + def test_cycle_no_weights_or_num(self): + with self.assertRaises(IndexError): + retworkx.generators.cycle_graph() diff --git a/tests/generators/test_path.py b/tests/generators/test_path.py new file mode 100644 index 0000000000..539076cc7f --- /dev/null +++ b/tests/generators/test_path.py @@ -0,0 +1,55 @@ +# 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 TestPathGraph(unittest.TestCase): + + def test_directed_path_graph(self): + graph = retworkx.generators.directed_path_graph(20) + self.assertEqual(len(graph), 20) + 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), []) + + def test_directed_path_graph_weights(self): + graph = retworkx.generators.directed_path_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), []) + + def test_path_directed_no_weights_or_num(self): + with self.assertRaises(IndexError): + retworkx.generators.directed_path_graph() + + def test_path_graph(self): + graph = retworkx.generators.path_graph(20) + self.assertEqual(len(graph), 20) + self.assertEqual(len(graph.edges()), 19) + + def test_path_graph_weights(self): + graph = retworkx.generators.path_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_path_no_weights_or_num(self): + with self.assertRaises(IndexError): + retworkx.generators.path_graph() diff --git a/tests/generators/test_star.py b/tests/generators/test_star.py new file mode 100644 index 0000000000..b262e1c1ed --- /dev/null +++ b/tests/generators/test_star.py @@ -0,0 +1,69 @@ +# 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 TestStarGraph(unittest.TestCase): + + def test_directed_star_graph(self): + graph = retworkx.generators.directed_star_graph(20) + self.assertEqual(len(graph), 20) + self.assertEqual(len(graph.edges()), 19) + expected_edges = [(0, i, None) for i in range(1, 20)] + self.assertEqual(sorted(graph.out_edges(0)), sorted(expected_edges)) + + def test_star_directed_graph_inward(self): + graph = retworkx.generators.directed_star_graph(20, inward=True) + self.assertEqual(len(graph), 20) + self.assertEqual(len(graph.edges()), 19) + expected_edges = [(i, 0, None) for i in range(1, 20)] + self.assertEqual(sorted(graph.in_edges(0)), sorted(expected_edges)) + + def test_directed_star_graph_weights(self): + graph = retworkx.generators.directed_star_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) + expected_edges = sorted([(0, i, None) for i in range(1, 20)]) + self.assertEqual(sorted(graph.out_edges(0)), expected_edges) + + def test_star_directed_graph_weights_inward(self): + graph = retworkx.generators.directed_star_graph( + weights=list(range(20)), inward=True) + self.assertEqual(len(graph), 20) + self.assertEqual([x for x in range(20)], graph.nodes()) + self.assertEqual(len(graph.edges()), 19) + expected_edges = [(i, 0, None) for i in range(1, 20)] + self.assertEqual(sorted(graph.in_edges(0)), sorted(expected_edges)) + + def test_star_directed_no_weights_or_num(self): + with self.assertRaises(IndexError): + retworkx.generators.directed_star_graph() + + def test_star_graph(self): + graph = retworkx.generators.star_graph(20) + self.assertEqual(len(graph), 20) + self.assertEqual(len(graph.edges()), 19) + + def test_star_graph_weights(self): + graph = retworkx.generators.star_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_star_no_weights_or_num(self): + with self.assertRaises(IndexError): + retworkx.generators.star_graph()