rust_igraph/algorithms/community/
modularity_matrix.rs1use crate::core::{Graph, IgraphError, IgraphResult};
15
16pub fn modularity_matrix(
47 graph: &Graph,
48 weights: Option<&[f64]>,
49 resolution: f64,
50 directed: bool,
51) -> IgraphResult<Vec<Vec<f64>>> {
52 let n = graph.vcount();
53 let ecount = graph.ecount();
54
55 if !resolution.is_finite() || resolution < 0.0 {
56 return Err(IgraphError::InvalidArgument(
57 "resolution parameter must be non-negative and finite".to_string(),
58 ));
59 }
60 if let Some(w) = weights {
61 if w.len() != ecount {
62 return Err(IgraphError::InvalidArgument(format!(
63 "modularity_matrix: weights length ({}) does not match edge count ({ecount})",
64 w.len()
65 )));
66 }
67 }
68
69 if n == 0 {
70 return Ok(Vec::new());
71 }
72
73 let n_usize = n as usize;
74 let directed = directed && graph.is_directed();
75
76 let mut matrix = vec![vec![0.0_f64; n_usize]; n_usize];
78
79 let m_u32 =
80 u32::try_from(ecount).map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
81
82 for eid in 0..m_u32 {
83 let (from, to) = graph.edge(eid)?;
84 let f = from as usize;
85 let t = to as usize;
86 let w = weights.map_or(1.0, |ws| ws[eid as usize]);
87 matrix[f][t] += w;
88 if !directed {
89 matrix[t][f] += w;
90 }
91 }
92
93 #[allow(clippy::cast_precision_loss)]
95 let sw: f64 = weights.map_or(ecount as f64, |ws| ws.iter().sum());
96 if sw == 0.0 {
97 return Err(IgraphError::InvalidArgument(
98 "modularity_matrix: total edge weight is zero; matrix is undefined".to_string(),
99 ));
100 }
101
102 if directed {
103 let mut k_out = vec![0.0_f64; n_usize];
105 let mut k_in = vec![0.0_f64; n_usize];
106
107 for eid in 0..m_u32 {
108 let (from, to) = graph.edge(eid)?;
109 let w = weights.map_or(1.0, |ws| ws[eid as usize]);
110 k_out[from as usize] += w;
111 k_in[to as usize] += w;
112 }
113
114 let scaling = resolution / sw;
115 for (j, &k_in_j) in k_in.iter().enumerate().take(n_usize) {
116 for i in 0..n_usize {
117 matrix[i][j] -= k_out[i] * k_in_j * scaling;
118 }
119 }
120 } else {
121 let mut deg = vec![0.0_f64; n_usize];
123
124 for eid in 0..m_u32 {
125 let (from, to) = graph.edge(eid)?;
126 let w = weights.map_or(1.0, |ws| ws[eid as usize]);
127 deg[from as usize] += w;
128 deg[to as usize] += w;
129 }
130
131 let scaling = resolution / (2.0 * sw);
132 for j in 0..n_usize {
133 for i in 0..n_usize {
134 matrix[i][j] -= deg[i] * deg[j] * scaling;
135 }
136 }
137 }
138
139 Ok(matrix)
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 fn close(a: f64, b: f64) -> bool {
147 (a - b).abs() < 1e-10
148 }
149
150 #[test]
151 fn empty_graph() {
152 let g = Graph::with_vertices(0);
153 let m = modularity_matrix(&g, None, 1.0, false).unwrap();
154 assert!(m.is_empty());
155 }
156
157 #[test]
158 fn single_vertex_no_edges_error() {
159 let g = Graph::with_vertices(1);
160 assert!(modularity_matrix(&g, None, 1.0, false).is_err());
162 }
163
164 #[test]
165 fn triangle_undirected() {
166 let mut g = Graph::with_vertices(3);
167 g.add_edge(0, 1).unwrap();
168 g.add_edge(1, 2).unwrap();
169 g.add_edge(2, 0).unwrap();
170 let b = modularity_matrix(&g, None, 1.0, false).unwrap();
171 for (i, row) in b.iter().enumerate() {
173 assert!(close(row[i], -2.0 / 3.0));
174 for (j, &val) in row.iter().enumerate() {
175 if i != j {
176 assert!(close(val, 1.0 / 3.0));
177 }
178 }
179 }
180 }
181
182 #[test]
183 fn path_3_undirected() {
184 let mut g = Graph::with_vertices(3);
186 g.add_edge(0, 1).unwrap();
187 g.add_edge(1, 2).unwrap();
188 let b = modularity_matrix(&g, None, 1.0, false).unwrap();
189 assert!(close(b[0][0], -0.25));
196 assert!(close(b[0][1], 0.5));
197 assert!(close(b[0][2], -0.25));
198 assert!(close(b[1][0], 0.5));
199 assert!(close(b[1][1], -1.0));
200 assert!(close(b[1][2], 0.5));
201 assert!(close(b[2][0], -0.25));
202 assert!(close(b[2][1], 0.5));
203 assert!(close(b[2][2], -0.25));
204 }
205
206 #[test]
207 fn directed_simple() {
208 let mut g = Graph::new(3, true).unwrap();
213 g.add_edge(0, 1).unwrap();
214 g.add_edge(1, 2).unwrap();
215 let b = modularity_matrix(&g, None, 1.0, true).unwrap();
216 assert!(close(b[0][0], 0.0));
226 assert!(close(b[0][1], 0.5));
227 assert!(close(b[0][2], -0.5));
228 assert!(close(b[1][0], 0.0));
229 assert!(close(b[1][1], -0.5));
230 assert!(close(b[1][2], 0.5));
231 assert!(close(b[2][0], 0.0));
232 assert!(close(b[2][1], 0.0));
233 assert!(close(b[2][2], 0.0));
234 }
235
236 #[test]
237 fn directed_flag_ignored_for_undirected_graph() {
238 let mut g = Graph::with_vertices(3);
239 g.add_edge(0, 1).unwrap();
240 g.add_edge(1, 2).unwrap();
241 g.add_edge(2, 0).unwrap();
242 let b1 = modularity_matrix(&g, None, 1.0, true).unwrap();
243 let b2 = modularity_matrix(&g, None, 1.0, false).unwrap();
244 for (row1, row2) in b1.iter().zip(b2.iter()) {
245 for (&v1, &v2) in row1.iter().zip(row2.iter()) {
246 assert!(close(v1, v2));
247 }
248 }
249 }
250
251 #[test]
252 fn weighted_undirected() {
253 let mut g = Graph::with_vertices(3);
262 g.add_edge(0, 1).unwrap();
263 g.add_edge(1, 2).unwrap();
264 let weights = vec![3.0, 1.0];
265 let b = modularity_matrix(&g, Some(&weights), 1.0, false).unwrap();
266 assert!(close(b[0][0], -9.0 / 8.0));
267 assert!(close(b[0][1], 3.0 - 12.0 / 8.0));
268 assert!(close(b[0][2], -3.0 / 8.0));
269 assert!(close(b[1][0], 3.0 - 12.0 / 8.0));
270 assert!(close(b[1][1], -16.0 / 8.0));
271 assert!(close(b[1][2], 1.0 - 4.0 / 8.0));
272 assert!(close(b[2][0], -3.0 / 8.0));
273 assert!(close(b[2][1], 1.0 - 4.0 / 8.0));
274 assert!(close(b[2][2], -1.0 / 8.0));
275 }
276
277 #[test]
278 fn self_loop_undirected() {
279 let mut g = Graph::with_vertices(2);
288 g.add_edge(0, 0).unwrap();
289 g.add_edge(0, 1).unwrap();
290 let b = modularity_matrix(&g, None, 1.0, false).unwrap();
291 assert!(close(b[0][0], -1.0 / 4.0));
292 assert!(close(b[0][1], 1.0 / 4.0));
293 assert!(close(b[1][0], 1.0 / 4.0));
294 assert!(close(b[1][1], -1.0 / 4.0));
295 }
296
297 #[test]
298 fn resolution_parameter() {
299 let mut g = Graph::with_vertices(3);
301 g.add_edge(0, 1).unwrap();
302 g.add_edge(1, 2).unwrap();
303 g.add_edge(2, 0).unwrap();
304 let b = modularity_matrix(&g, None, 0.0, false).unwrap();
305 assert!(close(b[0][0], 0.0));
307 assert!(close(b[0][1], 1.0));
308 assert!(close(b[0][2], 1.0));
309 assert!(close(b[1][2], 1.0));
310 }
311
312 #[test]
313 fn negative_resolution_errors() {
314 let mut g = Graph::with_vertices(2);
315 g.add_edge(0, 1).unwrap();
316 assert!(modularity_matrix(&g, None, -1.0, false).is_err());
317 }
318
319 #[test]
320 fn weights_length_mismatch_errors() {
321 let mut g = Graph::with_vertices(2);
322 g.add_edge(0, 1).unwrap();
323 assert!(modularity_matrix(&g, Some(&[1.0, 2.0]), 1.0, false).is_err());
324 }
325
326 #[test]
327 fn row_sums_zero_for_undirected_gamma_1() {
328 let mut g = Graph::with_vertices(4);
331 g.add_edge(0, 1).unwrap();
332 g.add_edge(1, 2).unwrap();
333 g.add_edge(2, 3).unwrap();
334 g.add_edge(3, 0).unwrap();
335 let b = modularity_matrix(&g, None, 1.0, false).unwrap();
336 for row in &b {
337 let row_sum: f64 = row.iter().sum();
338 assert!(close(row_sum, 0.0));
339 }
340 }
341}