diff --git a/releasenotes/notes/karate-club-35708b3838689a0b.yaml b/releasenotes/notes/karate-club-35708b3838689a0b.yaml new file mode 100644 index 000000000..5d47a385a --- /dev/null +++ b/releasenotes/notes/karate-club-35708b3838689a0b.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + Added a new function, :func:`~rustworkx.generators.karate_club_graph` that + returns Zachary's Karate Club graph, commonly found in social network examples. + + .. jupyter-execute:: + + import rustworkx.generators + from rustworkx.visualization import mpl_draw + + graph = rustworkx.generators.karate_club_graph() + layout = rustworkx.circular_layout(graph) + mpl_draw(graph, pos=layout) diff --git a/rustworkx-core/src/generators/karate_club.rs b/rustworkx-core/src/generators/karate_club.rs new file mode 100644 index 000000000..d0e0ac495 --- /dev/null +++ b/rustworkx-core/src/generators/karate_club.rs @@ -0,0 +1,126 @@ +// 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::hash::Hash; + +use petgraph::data::{Build, Create}; +use petgraph::visit::{Data, NodeIndexable}; + +/// Generates Zachary's Karate Club graph. +/// +/// Zachary's Karate Club graph is a well-known social network that represents +/// the relations between 34 members of a karate club. +/// Arguments: +/// +/// * `default_node_weight` - A callable that will receive a boolean, indicating +/// if a node is part of Mr Hi's faction (True) or the Officer's faction (false). +/// It shoudl return the node weight according to the desired type. +/// * `default_edge_weight` - A callable that will receive the integer representing +/// the strenght of the relation between two nodes. It should return the edge +/// weight according to the desired type. +/// +pub fn karate_club_graph(mut default_node_weight: F, mut default_edge_weight: H) -> G +where + G: Build + Create + Data + NodeIndexable, + F: FnMut(bool) -> T, + H: FnMut(usize) -> M, + G::NodeId: Eq + Hash, +{ + const N: usize = 34; + const M: usize = 78; + let mr_hi_members: [u8; 17] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 16, 17, 19, 21]; + let membership: std::collections::HashSet = mr_hi_members.into_iter().collect(); + + let adjacency_list: Vec> = vec![ + vec![], + vec![(0, 4)], + vec![(0, 5), (1, 6)], + vec![(0, 3), (1, 3), (2, 3)], + vec![(0, 3)], + vec![(0, 3)], + vec![(0, 3), (4, 2), (5, 5)], + vec![(0, 2), (1, 4), (2, 4), (3, 3)], + vec![(0, 2), (2, 5)], + vec![(2, 1)], + vec![(0, 2), (4, 3), (5, 3)], + vec![(0, 3)], + vec![(0, 1), (3, 3)], + vec![(0, 3), (1, 5), (2, 3), (3, 3)], + vec![], + vec![], + vec![(5, 3), (6, 3)], + vec![(0, 2), (1, 1)], + vec![], + vec![(0, 2), (1, 2)], + vec![], + vec![(0, 2), (1, 2)], + vec![], + vec![], + vec![], + vec![(23, 5), (24, 2)], + vec![], + vec![(2, 2), (23, 4), (24, 3)], + vec![(2, 2)], + vec![(23, 3), (26, 4)], + vec![(1, 2), (8, 3)], + vec![(0, 2), (24, 2), (25, 7), (28, 2)], + vec![ + (2, 2), + (8, 3), + (14, 3), + (15, 3), + (18, 1), + (20, 3), + (22, 2), + (23, 5), + (29, 4), + (30, 3), + (31, 4), + ], + vec![ + (8, 4), + (9, 2), + (13, 3), + (14, 2), + (15, 4), + (18, 2), + (19, 1), + (20, 1), + (23, 4), + (26, 2), + (27, 4), + (28, 2), + (29, 2), + (30, 3), + (31, 4), + (32, 5), + (22, 3), + ], + ]; + + let mut graph = G::with_capacity(N, M); + + let mut node_indices = Vec::with_capacity(N); + for (row, neighbors) in adjacency_list.into_iter().enumerate() { + let node_id = graph.add_node(default_node_weight(membership.contains(&(row as u8)))); + node_indices.push(node_id); + + for (neighbor, weight) in neighbors.into_iter() { + graph.add_edge( + node_indices[neighbor], + node_indices[row], + default_edge_weight(weight), + ); + } + } + graph +} diff --git a/rustworkx-core/src/generators/mod.rs b/rustworkx-core/src/generators/mod.rs index 6c0af1ace..d7a5ddd83 100644 --- a/rustworkx-core/src/generators/mod.rs +++ b/rustworkx-core/src/generators/mod.rs @@ -22,6 +22,7 @@ mod grid_graph; mod heavy_hex_graph; mod heavy_square_graph; mod hexagonal_lattice_graph; +mod karate_club; mod lollipop_graph; mod path_graph; mod petersen_graph; @@ -55,6 +56,7 @@ pub use grid_graph::grid_graph; pub use heavy_hex_graph::heavy_hex_graph; pub use heavy_square_graph::heavy_square_graph; pub use hexagonal_lattice_graph::{hexagonal_lattice_graph, hexagonal_lattice_graph_weighted}; +pub use karate_club::karate_club_graph; pub use lollipop_graph::lollipop_graph; pub use path_graph::path_graph; pub use petersen_graph::petersen_graph; diff --git a/rustworkx/generators/__init__.pyi b/rustworkx/generators/__init__.pyi index 440db3940..6136cb3de 100644 --- a/rustworkx/generators/__init__.pyi +++ b/rustworkx/generators/__init__.pyi @@ -132,3 +132,4 @@ def directed_complete_graph( multigraph: bool = ..., ) -> PyDiGraph: ... def dorogovtsev_goltsev_mendes_graph(n: int) -> PyGraph: ... +def karate_club_graph(multigraph: bool = ...) -> PyGraph: ... diff --git a/src/generators.rs b/src/generators.rs index ac8088d12..82b75b8a4 100644 --- a/src/generators.rs +++ b/src/generators.rs @@ -1715,6 +1715,59 @@ pub fn dorogovtsev_goltsev_mendes_graph(py: Python, n: usize) -> PyResult PyResult { + let default_node_fn = |w: bool| match w { + true => "Mr. Hi".to_object(py), + false => "Officer".to_object(py), + }; + let default_edge_fn = |w: usize| (w as f64).to_object(py); + let graph: StablePyGraph = + core_generators::karate_club_graph(default_node_fn, default_edge_fn); + Ok(graph::PyGraph { + graph, + node_removed: false, + multigraph, + attrs: py.None(), + }) +} + #[pymodule] pub fn generators(_py: Python, m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(cycle_graph))?; @@ -1744,5 +1797,6 @@ pub fn generators(_py: Python, m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(complete_graph))?; m.add_wrapped(wrap_pyfunction!(directed_complete_graph))?; m.add_wrapped(wrap_pyfunction!(dorogovtsev_goltsev_mendes_graph))?; + m.add_wrapped(wrap_pyfunction!(karate_club_graph))?; Ok(()) } diff --git a/tests/graph/test_karate.py b/tests/graph/test_karate.py new file mode 100644 index 000000000..feb99cfe8 --- /dev/null +++ b/tests/graph/test_karate.py @@ -0,0 +1,402 @@ +# 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 tempfile + +import rustworkx as rx + + +class TestKarate(unittest.TestCase): + def test_isomorphic_to_networkx(self): + def node_matcher(a, b): + if isinstance(a, dict): + ( + a, + b, + ) = ( + b, + a, + ) + return a == b["club"] + + def edge_matcher(a, b): + if isinstance(a, dict): + ( + a, + b, + ) = ( + b, + a, + ) + return a == b["weight"] + + with tempfile.NamedTemporaryFile("wt") as fd: + fd.write(karate_xml) + fd.flush() + nx_graph = rx.read_graphml(fd.name)[0] + + graph = rx.generators.karate_club_graph() + + self.assertTrue( + rx.is_isomorphic(graph, nx_graph, node_matcher=node_matcher, edge_matcher=edge_matcher) + ) + + +# ruff: noqa: E501 +# Output of +# import networkx as nx +# nx.write_graphml_lxml(nx.karate_club_graph(), open("karate.xml", "w")) +karate_xml = """ + + + + +Zachary's Karate Club + + Mr. Hi + + + Mr. Hi + + + Mr. Hi + + + Mr. Hi + + + Mr. Hi + + + Mr. Hi + + + Mr. Hi + + + Mr. Hi + + + Mr. Hi + + + Officer + + + Mr. Hi + + + Mr. Hi + + + Mr. Hi + + + Mr. Hi + + + Officer + + + Officer + + + Mr. Hi + + + Mr. Hi + + + Officer + + + Mr. Hi + + + Officer + + + Mr. Hi + + + Officer + + + Officer + + + Officer + + + Officer + + + Officer + + + Officer + + + Officer + + + Officer + + + Officer + + + Officer + + + Officer + + + Officer + + + 4 + + + 5 + + + 3 + + + 3 + + + 3 + + + 3 + + + 2 + + + 2 + + + 2 + + + 3 + + + 1 + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + 6 + + + 3 + + + 4 + + + 5 + + + 1 + + + 2 + + + 2 + + + 2 + + + 3 + + + 4 + + + 5 + + + 1 + + + 3 + + + 2 + + + 2 + + + 2 + + + 3 + + + 3 + + + 3 + + + 2 + + + 3 + + + 5 + + + 3 + + + 3 + + + 3 + + + 3 + + + 3 + + + 4 + + + 2 + + + 3 + + + 3 + + + 2 + + + 3 + + + 4 + + + 1 + + + 2 + + + 1 + + + 3 + + + 1 + + + 2 + + + 3 + + + 5 + + + 4 + + + 3 + + + 5 + + + 4 + + + 2 + + + 3 + + + 2 + + + 7 + + + 4 + + + 2 + + + 4 + + + 2 + + + 2 + + + 4 + + + 2 + + + 3 + + + 3 + + + 4 + + + 4 + + + 5 + + +"""