Skip to content

Commit d9fbe83

Browse files
IvanIsCodingalexanderivriimtreinish
authored
Add Zachary's Karate Club (#1280)
* Karate club draft * Add Zachary's data * Consider weight for Karate Club * Add tests * Add karate_club_graph signature * Fix clippy * Update rustworkx-core/src/generators/karate_club.rs Co-authored-by: Alexander Ivrii <[email protected]> * Add labels and documentation * Test node labels * Add simple Rust docstring * Add release notes * Black changes * Update test to be independent * Apply suggestions from code review Co-authored-by: Matthew Treinish <[email protected]> * Format and u8 * Ignore ruff for that file * Use adjacency list * Add some documentation * Update tests/graph/test_karate.py --------- Co-authored-by: Alexander Ivrii <[email protected]> Co-authored-by: Matthew Treinish <[email protected]>
1 parent cb897b9 commit d9fbe83

File tree

6 files changed

+599
-0
lines changed

6 files changed

+599
-0
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
features:
3+
- |
4+
Added a new function, :func:`~rustworkx.generators.karate_club_graph` that
5+
returns Zachary's Karate Club graph, commonly found in social network examples.
6+
7+
.. jupyter-execute::
8+
9+
import rustworkx.generators
10+
from rustworkx.visualization import mpl_draw
11+
12+
graph = rustworkx.generators.karate_club_graph()
13+
layout = rustworkx.circular_layout(graph)
14+
mpl_draw(graph, pos=layout)
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
// not use this file except in compliance with the License. You may obtain
3+
// a copy of the License at
4+
//
5+
// http://www.apache.org/licenses/LICENSE-2.0
6+
//
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
// License for the specific language governing permissions and limitations
11+
// under the License.
12+
13+
use std::hash::Hash;
14+
15+
use petgraph::data::{Build, Create};
16+
use petgraph::visit::{Data, NodeIndexable};
17+
18+
/// Generates Zachary's Karate Club graph.
19+
///
20+
/// Zachary's Karate Club graph is a well-known social network that represents
21+
/// the relations between 34 members of a karate club.
22+
/// Arguments:
23+
///
24+
/// * `default_node_weight` - A callable that will receive a boolean, indicating
25+
/// if a node is part of Mr Hi's faction (True) or the Officer's faction (false).
26+
/// It shoudl return the node weight according to the desired type.
27+
/// * `default_edge_weight` - A callable that will receive the integer representing
28+
/// the strenght of the relation between two nodes. It should return the edge
29+
/// weight according to the desired type.
30+
///
31+
pub fn karate_club_graph<G, T, F, H, M>(mut default_node_weight: F, mut default_edge_weight: H) -> G
32+
where
33+
G: Build + Create + Data<NodeWeight = T, EdgeWeight = M> + NodeIndexable,
34+
F: FnMut(bool) -> T,
35+
H: FnMut(usize) -> M,
36+
G::NodeId: Eq + Hash,
37+
{
38+
const N: usize = 34;
39+
const M: usize = 78;
40+
let mr_hi_members: [u8; 17] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 16, 17, 19, 21];
41+
let membership: std::collections::HashSet<u8> = mr_hi_members.into_iter().collect();
42+
43+
let adjacency_list: Vec<Vec<(usize, usize)>> = vec![
44+
vec![],
45+
vec![(0, 4)],
46+
vec![(0, 5), (1, 6)],
47+
vec![(0, 3), (1, 3), (2, 3)],
48+
vec![(0, 3)],
49+
vec![(0, 3)],
50+
vec![(0, 3), (4, 2), (5, 5)],
51+
vec![(0, 2), (1, 4), (2, 4), (3, 3)],
52+
vec![(0, 2), (2, 5)],
53+
vec![(2, 1)],
54+
vec![(0, 2), (4, 3), (5, 3)],
55+
vec![(0, 3)],
56+
vec![(0, 1), (3, 3)],
57+
vec![(0, 3), (1, 5), (2, 3), (3, 3)],
58+
vec![],
59+
vec![],
60+
vec![(5, 3), (6, 3)],
61+
vec![(0, 2), (1, 1)],
62+
vec![],
63+
vec![(0, 2), (1, 2)],
64+
vec![],
65+
vec![(0, 2), (1, 2)],
66+
vec![],
67+
vec![],
68+
vec![],
69+
vec![(23, 5), (24, 2)],
70+
vec![],
71+
vec![(2, 2), (23, 4), (24, 3)],
72+
vec![(2, 2)],
73+
vec![(23, 3), (26, 4)],
74+
vec![(1, 2), (8, 3)],
75+
vec![(0, 2), (24, 2), (25, 7), (28, 2)],
76+
vec![
77+
(2, 2),
78+
(8, 3),
79+
(14, 3),
80+
(15, 3),
81+
(18, 1),
82+
(20, 3),
83+
(22, 2),
84+
(23, 5),
85+
(29, 4),
86+
(30, 3),
87+
(31, 4),
88+
],
89+
vec![
90+
(8, 4),
91+
(9, 2),
92+
(13, 3),
93+
(14, 2),
94+
(15, 4),
95+
(18, 2),
96+
(19, 1),
97+
(20, 1),
98+
(23, 4),
99+
(26, 2),
100+
(27, 4),
101+
(28, 2),
102+
(29, 2),
103+
(30, 3),
104+
(31, 4),
105+
(32, 5),
106+
(22, 3),
107+
],
108+
];
109+
110+
let mut graph = G::with_capacity(N, M);
111+
112+
let mut node_indices = Vec::with_capacity(N);
113+
for (row, neighbors) in adjacency_list.into_iter().enumerate() {
114+
let node_id = graph.add_node(default_node_weight(membership.contains(&(row as u8))));
115+
node_indices.push(node_id);
116+
117+
for (neighbor, weight) in neighbors.into_iter() {
118+
graph.add_edge(
119+
node_indices[neighbor],
120+
node_indices[row],
121+
default_edge_weight(weight),
122+
);
123+
}
124+
}
125+
graph
126+
}

rustworkx-core/src/generators/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ mod grid_graph;
2222
mod heavy_hex_graph;
2323
mod heavy_square_graph;
2424
mod hexagonal_lattice_graph;
25+
mod karate_club;
2526
mod lollipop_graph;
2627
mod path_graph;
2728
mod petersen_graph;
@@ -55,6 +56,7 @@ pub use grid_graph::grid_graph;
5556
pub use heavy_hex_graph::heavy_hex_graph;
5657
pub use heavy_square_graph::heavy_square_graph;
5758
pub use hexagonal_lattice_graph::{hexagonal_lattice_graph, hexagonal_lattice_graph_weighted};
59+
pub use karate_club::karate_club_graph;
5860
pub use lollipop_graph::lollipop_graph;
5961
pub use path_graph::path_graph;
6062
pub use petersen_graph::petersen_graph;

rustworkx/generators/__init__.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,4 @@ def directed_complete_graph(
132132
multigraph: bool = ...,
133133
) -> PyDiGraph: ...
134134
def dorogovtsev_goltsev_mendes_graph(n: int) -> PyGraph: ...
135+
def karate_club_graph(multigraph: bool = ...) -> PyGraph: ...

src/generators.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1715,6 +1715,59 @@ pub fn dorogovtsev_goltsev_mendes_graph(py: Python, n: usize) -> PyResult<graph:
17151715
})
17161716
}
17171717

1718+
/// Generates Zachary's Karate Club graph.
1719+
///
1720+
/// Zachary's Karate Club graph is a well-known social network that represents
1721+
/// the relations between 34 members of a karate club, as described in the
1722+
/// paper by Zachary [1]_.
1723+
///
1724+
/// The graph is undirected and contains 34 nodes (members) and 78 edges
1725+
/// (interactions). Each node represents a club member, and each edge represents
1726+
/// a relationship between two members.
1727+
///
1728+
/// :param bool multigraph: When set to ``False`` the output
1729+
/// :class:`~rustworkx.PyGraph` object will not be not be a multigraph and
1730+
/// won't allow parallel edges to be added. Instead
1731+
/// calls which would create a parallel edge will update the existing edge.
1732+
///
1733+
/// :returns: The undirected graph representing the Karate club graph.
1734+
/// Each node is labeled according to its assigned faction.
1735+
///
1736+
/// :rtype: PyGraph
1737+
///
1738+
/// .. jupyter-execute::
1739+
///
1740+
/// import rustworkx.generators
1741+
/// from rustworkx.visualization import mpl_draw
1742+
///
1743+
/// graph = rustworkx.generators.karate_club_graph()
1744+
/// layout = rustworkx.circular_layout(graph)
1745+
/// mpl_draw(graph, pos=layout)
1746+
///
1747+
/// .. [1] Zachary, Wayne W.
1748+
/// "An information flow model for conflict and fission in small groups"
1749+
/// Journal of Anthropological Research, 33, 452-473.
1750+
/// https://doi.org/10.1086/jar.33.4.3629752
1751+
#[pyfunction]
1752+
#[pyo3(
1753+
signature=(multigraph=true),
1754+
)]
1755+
pub fn karate_club_graph(py: Python, multigraph: bool) -> PyResult<graph::PyGraph> {
1756+
let default_node_fn = |w: bool| match w {
1757+
true => "Mr. Hi".to_object(py),
1758+
false => "Officer".to_object(py),
1759+
};
1760+
let default_edge_fn = |w: usize| (w as f64).to_object(py);
1761+
let graph: StablePyGraph<Undirected> =
1762+
core_generators::karate_club_graph(default_node_fn, default_edge_fn);
1763+
Ok(graph::PyGraph {
1764+
graph,
1765+
node_removed: false,
1766+
multigraph,
1767+
attrs: py.None(),
1768+
})
1769+
}
1770+
17181771
#[pymodule]
17191772
pub fn generators(_py: Python, m: &Bound<PyModule>) -> PyResult<()> {
17201773
m.add_wrapped(wrap_pyfunction!(cycle_graph))?;
@@ -1744,5 +1797,6 @@ pub fn generators(_py: Python, m: &Bound<PyModule>) -> PyResult<()> {
17441797
m.add_wrapped(wrap_pyfunction!(complete_graph))?;
17451798
m.add_wrapped(wrap_pyfunction!(directed_complete_graph))?;
17461799
m.add_wrapped(wrap_pyfunction!(dorogovtsev_goltsev_mendes_graph))?;
1800+
m.add_wrapped(wrap_pyfunction!(karate_club_graph))?;
17471801
Ok(())
17481802
}

0 commit comments

Comments
 (0)