From 503930f162095fe4e2669688196d0fa75a53d0c3 Mon Sep 17 00:00:00 2001 From: dfacoet <11651258+dfacoet@users.noreply.github.com> Date: Fri, 31 May 2024 09:30:20 +0100 Subject: [PATCH 1/6] Add DGM graph generator to rustworkx-core --- .../dorogovtsev_goltsev_mendes_graph.rs | 59 +++++++++++++++++++ rustworkx-core/src/generators/mod.rs | 2 + 2 files changed, 61 insertions(+) create mode 100644 rustworkx-core/src/generators/dorogovtsev_goltsev_mendes_graph.rs diff --git a/rustworkx-core/src/generators/dorogovtsev_goltsev_mendes_graph.rs b/rustworkx-core/src/generators/dorogovtsev_goltsev_mendes_graph.rs new file mode 100644 index 000000000..e92f5e7d1 --- /dev/null +++ b/rustworkx-core/src/generators/dorogovtsev_goltsev_mendes_graph.rs @@ -0,0 +1,59 @@ +// 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 petgraph::{ + data::Create, + visit::{Data, EdgeIndexable}, +}; + +use super::InvalidInputError; + +// TODO: docs +pub fn dorogovtsev_goltsev_mendes_graph( + t: isize, + mut default_node_weight: F, + mut default_edge_weight: H, +) -> Result +where + G: Create + Data, + F: FnMut() -> T, + H: FnMut() -> M, +{ + if t < -1 { + return Err(InvalidInputError {}); + } + let n_edges = usize::pow(3, t as u32 + 1); // Check against overflow? + let n_nodes = (n_edges + 3) / 2; + let mut graph = G::with_capacity(n_nodes, n_edges); + + let node_0 = graph.add_node(default_node_weight()); + let node_1 = graph.add_node(default_node_weight()); + graph + .add_edge(node_0, node_1, default_edge_weight()) + .unwrap(); + let mut current_endpoints = vec![(node_0, node_1)]; + + for _ in 0..t + 1 { + let mut new_endpoints = vec![]; + for (source, target) in current_endpoints.iter() { + let new_node = graph.add_node(default_node_weight()); + graph.add_edge(*source, new_node, default_edge_weight()); + new_endpoints.push((*source, new_node)); + graph.add_edge(*target, new_node, default_edge_weight()); + new_endpoints.push((*target, new_node)); + } + current_endpoints.extend(new_endpoints); + } + Ok(graph) +} + +// TODO: tests diff --git a/rustworkx-core/src/generators/mod.rs b/rustworkx-core/src/generators/mod.rs index 3034baab1..bde826cbf 100644 --- a/rustworkx-core/src/generators/mod.rs +++ b/rustworkx-core/src/generators/mod.rs @@ -16,6 +16,7 @@ mod barbell_graph; mod binomial_tree_graph; mod complete_graph; mod cycle_graph; +mod dorogovtsev_goltsev_mendes_graph; mod full_rary_tree_graph; mod grid_graph; mod heavy_hex_graph; @@ -48,6 +49,7 @@ pub use barbell_graph::barbell_graph; pub use binomial_tree_graph::binomial_tree_graph; pub use complete_graph::complete_graph; pub use cycle_graph::cycle_graph; +pub use dorogovtsev_goltsev_mendes_graph::dorogovtsev_goltsev_mendes_graph; pub use full_rary_tree_graph::full_rary_tree_graph; pub use grid_graph::grid_graph; pub use heavy_hex_graph::heavy_hex_graph; From d672394eb77d926fd75ec91290f01c18bcd23df1 Mon Sep 17 00:00:00 2001 From: Davide Facoetti <11651258+dfacoet@users.noreply.github.com> Date: Fri, 31 May 2024 13:42:08 +0100 Subject: [PATCH 2/6] Add DGM graph generator to Python library --- src/generators.rs | 18 +++++++++++++++ .../test_dorogovtsev_goltsev_mendes.py | 23 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 tests/generators/test_dorogovtsev_goltsev_mendes.py diff --git a/src/generators.rs b/src/generators.rs index 8f6af702f..34a5a7a5e 100644 --- a/src/generators.rs +++ b/src/generators.rs @@ -1618,6 +1618,23 @@ pub fn directed_complete_graph( }) } +/// TODO: docs +#[pyfunction] +#[pyo3(signature=(t,))] +pub fn dorogovtsev_goltsev_mendes_graph(py: Python, t: isize) -> PyResult { + let default_fn = || py.None(); + let graph = match core_generators::dorogovtsev_goltsev_mendes_graph(t, default_fn, default_fn) { + Ok(graph) => graph, + Err(_) => return Err(PyIndexError::new_err("t must be >= -1")), + }; + Ok(graph::PyGraph { + graph, + node_removed: false, + multigraph: false, + attrs: py.None(), + }) +} + #[pymodule] pub fn generators(_py: Python, m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(cycle_graph))?; @@ -1646,5 +1663,6 @@ pub fn generators(_py: Python, m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(directed_empty_graph))?; m.add_wrapped(wrap_pyfunction!(complete_graph))?; m.add_wrapped(wrap_pyfunction!(directed_complete_graph))?; + m.add_wrapped(wrap_pyfunction!(dorogovtsev_goltsev_mendes_graph))?; Ok(()) } diff --git a/tests/generators/test_dorogovtsev_goltsev_mendes.py b/tests/generators/test_dorogovtsev_goltsev_mendes.py new file mode 100644 index 000000000..76f404b1d --- /dev/null +++ b/tests/generators/test_dorogovtsev_goltsev_mendes.py @@ -0,0 +1,23 @@ +# 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 rustworkx + + +class TestDorogovtsevGoltsevMendesGraph(unittest.TestCase): + def test_dorogovtsev_goltsev_mendes_graph(self): + for t in range(6): + graph = rustworkx.generators.dorogovtsev_goltsev_mendes_graph(t) + self.assertEqual(len(graph), 3 * (3**t + 1) / 2) + self.assertEqual(len(graph.edges()), 3 ** (t + 1)) From dd57798b071919ae47d29701db3fe0d0719d393d Mon Sep 17 00:00:00 2001 From: dfacoet <11651258+dfacoet@users.noreply.github.com> Date: Fri, 31 May 2024 19:41:39 +0100 Subject: [PATCH 3/6] Change parameter t to n (state to transitions) --- .../generators/dorogovtsev_goltsev_mendes_graph.rs | 14 ++++---------- src/generators.rs | 6 +++--- .../generators/test_dorogovtsev_goltsev_mendes.py | 8 ++++---- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/rustworkx-core/src/generators/dorogovtsev_goltsev_mendes_graph.rs b/rustworkx-core/src/generators/dorogovtsev_goltsev_mendes_graph.rs index e92f5e7d1..287d037eb 100644 --- a/rustworkx-core/src/generators/dorogovtsev_goltsev_mendes_graph.rs +++ b/rustworkx-core/src/generators/dorogovtsev_goltsev_mendes_graph.rs @@ -10,16 +10,13 @@ // License for the specific language governing permissions and limitations // under the License. -use petgraph::{ - data::Create, - visit::{Data, EdgeIndexable}, -}; +use petgraph::{data::Create, visit::Data}; use super::InvalidInputError; // TODO: docs pub fn dorogovtsev_goltsev_mendes_graph( - t: isize, + n: usize, mut default_node_weight: F, mut default_edge_weight: H, ) -> Result @@ -28,10 +25,7 @@ where F: FnMut() -> T, H: FnMut() -> M, { - if t < -1 { - return Err(InvalidInputError {}); - } - let n_edges = usize::pow(3, t as u32 + 1); // Check against overflow? + let n_edges = usize::pow(3, n as u32); // Check against overflow? let n_nodes = (n_edges + 3) / 2; let mut graph = G::with_capacity(n_nodes, n_edges); @@ -42,7 +36,7 @@ where .unwrap(); let mut current_endpoints = vec![(node_0, node_1)]; - for _ in 0..t + 1 { + for _ in 0..n { let mut new_endpoints = vec![]; for (source, target) in current_endpoints.iter() { let new_node = graph.add_node(default_node_weight()); diff --git a/src/generators.rs b/src/generators.rs index 34a5a7a5e..3137016d7 100644 --- a/src/generators.rs +++ b/src/generators.rs @@ -1620,10 +1620,10 @@ pub fn directed_complete_graph( /// TODO: docs #[pyfunction] -#[pyo3(signature=(t,))] -pub fn dorogovtsev_goltsev_mendes_graph(py: Python, t: isize) -> PyResult { +#[pyo3(signature=(n,))] +pub fn dorogovtsev_goltsev_mendes_graph(py: Python, n: usize) -> PyResult { let default_fn = || py.None(); - let graph = match core_generators::dorogovtsev_goltsev_mendes_graph(t, default_fn, default_fn) { + let graph = match core_generators::dorogovtsev_goltsev_mendes_graph(n, default_fn, default_fn) { Ok(graph) => graph, Err(_) => return Err(PyIndexError::new_err("t must be >= -1")), }; diff --git a/tests/generators/test_dorogovtsev_goltsev_mendes.py b/tests/generators/test_dorogovtsev_goltsev_mendes.py index 76f404b1d..36139619c 100644 --- a/tests/generators/test_dorogovtsev_goltsev_mendes.py +++ b/tests/generators/test_dorogovtsev_goltsev_mendes.py @@ -17,7 +17,7 @@ class TestDorogovtsevGoltsevMendesGraph(unittest.TestCase): def test_dorogovtsev_goltsev_mendes_graph(self): - for t in range(6): - graph = rustworkx.generators.dorogovtsev_goltsev_mendes_graph(t) - self.assertEqual(len(graph), 3 * (3**t + 1) / 2) - self.assertEqual(len(graph.edges()), 3 ** (t + 1)) + for n in range(0, 6): + graph = rustworkx.generators.dorogovtsev_goltsev_mendes_graph(n) + self.assertEqual(len(graph), (3 ** n + 3) // 2) + self.assertEqual(len(graph.edges()), 3 ** n) From 399c14e4e24ae2736b0a022db2c79736e7d4c1a5 Mon Sep 17 00:00:00 2001 From: dfacoet <11651258+dfacoet@users.noreply.github.com> Date: Fri, 31 May 2024 21:12:53 +0100 Subject: [PATCH 4/6] Add rust tests and docs --- docs/source/api/generators.rst | 1 + .../dorogovtsev_goltsev_mendes_graph.rs | 83 ++++++++++++++++++- src/generators.rs | 26 +++++- .../test_dorogovtsev_goltsev_mendes.py | 4 +- 4 files changed, 108 insertions(+), 6 deletions(-) diff --git a/docs/source/api/generators.rst b/docs/source/api/generators.rst index 9e173eb6b..d6d630d13 100644 --- a/docs/source/api/generators.rst +++ b/docs/source/api/generators.rst @@ -32,3 +32,4 @@ Generators rustworkx.generators.directed_empty_graph rustworkx.generators.complete_graph rustworkx.generators.directed_complete_graph + rustworkx.generators.dorogovtsev_goltsev_mendes_graph diff --git a/rustworkx-core/src/generators/dorogovtsev_goltsev_mendes_graph.rs b/rustworkx-core/src/generators/dorogovtsev_goltsev_mendes_graph.rs index 287d037eb..99954f498 100644 --- a/rustworkx-core/src/generators/dorogovtsev_goltsev_mendes_graph.rs +++ b/rustworkx-core/src/generators/dorogovtsev_goltsev_mendes_graph.rs @@ -14,7 +14,40 @@ use petgraph::{data::Create, visit::Data}; use super::InvalidInputError; -// TODO: docs +/// Generate a Dorogovtsev-Goltsev-Mendes graph +/// +/// Generate a graph following the recursive procedure in [1]. +/// Starting from the two-node, one-edge graph, iterating `n` times generates +/// a graph with `(3**n + 3) // 2` nodes and `3**n` edges. +/// +/// +/// Arguments: +/// +/// * `n` - The number of iterations to perform. n=0 returns the two-node, one-edge graph. +/// * `default_node_weight` - A callable that will return the weight to use for newly created nodes. +/// * `default_edge_weight` - A callable that will return the weight object to use for newly created edges. +/// +/// # Example +/// ```rust +/// use rustworkx_core::petgraph; +/// use rustworkx_core::generators::dorogovtsev_goltsev_mendes_graph; +/// use rustworkx_core::petgraph::visit::EdgeRef; +/// +/// let g: petgraph::graph::UnGraph<(), ()> = dorogovtsev_goltsev_mendes_graph(2, || (), || ()).unwrap(); +/// assert_eq!(g.node_count(), 6); +/// assert_eq!( +/// vec![(0, 1), (0, 2), (1, 2), (0, 3), (1, 3), (0, 4), (2, 4), (1, 5), (2, 5)], +/// g.edge_references() +/// .map(|edge| (edge.source().index(), edge.target().index())) +/// .collect::>(), +/// ); +/// ``` +/// +/// .. [1] S. N. Dorogovtsev, A. V. Goltsev and J. F. F. Mendes +/// “Pseudofractal scale-free web” +/// Physical Review E 65, 066122, 2002 +/// https://arxiv.org/abs/cond-mat/0112143 +/// pub fn dorogovtsev_goltsev_mendes_graph( n: usize, mut default_node_weight: F, @@ -25,7 +58,7 @@ where F: FnMut() -> T, H: FnMut() -> M, { - let n_edges = usize::pow(3, n as u32); // Check against overflow? + let n_edges = usize::pow(3, n as u32); let n_nodes = (n_edges + 3) / 2; let mut graph = G::with_capacity(n_nodes, n_edges); @@ -50,4 +83,48 @@ where Ok(graph) } -// TODO: tests +#[cfg(test)] +mod tests { + use crate::generators::dorogovtsev_goltsev_mendes_graph; + use crate::petgraph::graph::Graph; + use crate::petgraph::visit::EdgeRef; + + #[test] + fn test_dorogovtsev_goltsev_mendes_graph() { + for n in 0..6 { + let graph: Graph<(), ()> = match dorogovtsev_goltsev_mendes_graph(n, || (), || ()) { + Ok(graph) => graph, + Err(_) => panic!("Error generating graph"), + }; + assert_eq!(graph.node_count(), (usize::pow(3, n as u32) + 3) / 2); + assert_eq!(graph.edge_count(), usize::pow(3, n as u32)); + } + } + + #[test] + fn test_dorogovtsev_goltsev_mendes_graph_edges() { + let n = 2; + let expected_edge_list = vec![ + (0, 1), + (0, 2), + (1, 2), + (0, 3), + (1, 3), + (0, 4), + (2, 4), + (1, 5), + (2, 5), + ]; + let graph: Graph<(), ()> = match dorogovtsev_goltsev_mendes_graph(n, || (), || ()) { + Ok(graph) => graph, + Err(_) => panic!("Error generating graph"), + }; + assert_eq!( + expected_edge_list, + graph + .edge_references() + .map(|edge| (edge.source().index(), edge.target().index())) + .collect::>(), + ) + } +} diff --git a/src/generators.rs b/src/generators.rs index 3137016d7..df4074d37 100644 --- a/src/generators.rs +++ b/src/generators.rs @@ -1618,7 +1618,31 @@ pub fn directed_complete_graph( }) } -/// TODO: docs +/// Generate a Dorogovtsev-Goltsev-Mendes graph. +/// +/// Generate a graph following the recursive procedure in [1]_ . +/// Starting from the two-node, one-edge graph, iterating `n` times generates +/// a graph with `(3**n + 3) // 2` nodes and `3**n` edges. +/// +/// :param int n: The number of iterations to perform. +/// +/// :returns: The generated Dorogovtsev-Goltsev-Mendes graph +/// +/// :rtype: PyGraph +/// +/// .. jupyter-execute:: +/// +/// import rustworkx.generators +/// from rustworkx.visualization import mpl_draw +/// +/// graph = rustworkx.generators.dorogovtsev_goltsev_mendes_graph(2) +/// mpl_draw(graph) +/// +/// .. [1] S. N. Dorogovtsev, A. V. Goltsev and J. F. F. Mendes +/// "Pseudofractal scale-free web" +/// Physical Review E 65, 066122, 2002 +/// https://arxiv.org/abs/cond-mat/0112143 +/// #[pyfunction] #[pyo3(signature=(n,))] pub fn dorogovtsev_goltsev_mendes_graph(py: Python, n: usize) -> PyResult { diff --git a/tests/generators/test_dorogovtsev_goltsev_mendes.py b/tests/generators/test_dorogovtsev_goltsev_mendes.py index 36139619c..2a5cbc1ff 100644 --- a/tests/generators/test_dorogovtsev_goltsev_mendes.py +++ b/tests/generators/test_dorogovtsev_goltsev_mendes.py @@ -19,5 +19,5 @@ class TestDorogovtsevGoltsevMendesGraph(unittest.TestCase): def test_dorogovtsev_goltsev_mendes_graph(self): for n in range(0, 6): graph = rustworkx.generators.dorogovtsev_goltsev_mendes_graph(n) - self.assertEqual(len(graph), (3 ** n + 3) // 2) - self.assertEqual(len(graph.edges()), 3 ** n) + self.assertEqual(len(graph), (3**n + 3) // 2) + self.assertEqual(len(graph.edges()), 3**n) From b217b56954e1ebed4e0f26ff374f1dc2e9da392f Mon Sep 17 00:00:00 2001 From: dfacoet <11651258+dfacoet@users.noreply.github.com> Date: Fri, 31 May 2024 22:06:18 +0100 Subject: [PATCH 5/6] Add stubs and release notes --- ...govtsev-goltsev-mendes-generator-d7fd575b693bd652.yaml | 8 ++++++++ rustworkx/generators/__init__.pyi | 1 + 2 files changed, 9 insertions(+) create mode 100644 releasenotes/notes/dorogovtsev-goltsev-mendes-generator-d7fd575b693bd652.yaml diff --git a/releasenotes/notes/dorogovtsev-goltsev-mendes-generator-d7fd575b693bd652.yaml b/releasenotes/notes/dorogovtsev-goltsev-mendes-generator-d7fd575b693bd652.yaml new file mode 100644 index 000000000..a8b8846ea --- /dev/null +++ b/releasenotes/notes/dorogovtsev-goltsev-mendes-generator-d7fd575b693bd652.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Added :func:`rustworkx.generators.dorogovtsev_goltsev_mendes_graph` that generates + deterministic scale-free graphs using the Dorogovtsev-Goltsev-Mendes iterative procedure. + - | + Added to rustworkx-for ``generators::dorogovtsev_goltsev_mendes_graph`` function that generates, + deterministic scale-free graphs using the Dorogovtsev-Goltsev-Mendes iterative procedure. diff --git a/rustworkx/generators/__init__.pyi b/rustworkx/generators/__init__.pyi index 2e295478c..03b626940 100644 --- a/rustworkx/generators/__init__.pyi +++ b/rustworkx/generators/__init__.pyi @@ -126,3 +126,4 @@ def directed_complete_graph( weights: Sequence[Any] | None = ..., multigraph: bool = ..., ) -> PyDiGraph: ... +def dorogovtsev_goltsev_mendes_graph(n: int) -> PyGraph: ... From ba0934b54e0d6c0f22a40cc660bf87ffff8c46d4 Mon Sep 17 00:00:00 2001 From: dfacoet <11651258+dfacoet@users.noreply.github.com> Date: Sat, 1 Jun 2024 14:45:29 +0100 Subject: [PATCH 6/6] Improve python tests --- tests/generators/test_dorogovtsev_goltsev_mendes.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/generators/test_dorogovtsev_goltsev_mendes.py b/tests/generators/test_dorogovtsev_goltsev_mendes.py index 2a5cbc1ff..1d89ee77d 100644 --- a/tests/generators/test_dorogovtsev_goltsev_mendes.py +++ b/tests/generators/test_dorogovtsev_goltsev_mendes.py @@ -18,6 +18,11 @@ class TestDorogovtsevGoltsevMendesGraph(unittest.TestCase): def test_dorogovtsev_goltsev_mendes_graph(self): for n in range(0, 6): - graph = rustworkx.generators.dorogovtsev_goltsev_mendes_graph(n) - self.assertEqual(len(graph), (3**n + 3) // 2) - self.assertEqual(len(graph.edges()), 3**n) + with self.subTest(n=n): + graph = rustworkx.generators.dorogovtsev_goltsev_mendes_graph(n) + self.assertEqual(len(graph), (3**n + 3) // 2) + self.assertEqual(len(graph.edges()), 3**n) + self.assertTrue(rustworkx.is_planar(graph)) + + def test_dorogovstev_goltsev_mendes_graph_error(self): + self.assertRaises(OverflowError, rustworkx.generators.dorogovtsev_goltsev_mendes_graph, -1)