rust_igraph/algorithms/properties/
modularity_ratios.rs1#![allow(
13 clippy::cast_lossless,
14 clippy::cast_possible_truncation,
15 clippy::cast_precision_loss,
16 clippy::many_single_char_names,
17 clippy::needless_range_loop,
18 clippy::similar_names,
19 clippy::too_many_lines
20)]
21
22use crate::core::{Graph, IgraphResult};
23
24pub fn modularity_upper_bound_ratio(graph: &Graph) -> IgraphResult<f64> {
44 let n = graph.vcount() as usize;
45 let m = graph.ecount();
46 if n < 2 || m == 0 {
47 return Ok(0.0);
48 }
49
50 let membership = greedy_communities(graph, n)?;
51 let q = compute_modularity(graph, n, m, &membership)?;
52
53 let k = *membership.iter().max().unwrap_or(&0) + 1;
54 if k <= 1 {
55 return Ok(0.0);
56 }
57
58 let q_max = 1.0 - 1.0 / k as f64;
59 if q_max < 1e-30 {
60 return Ok(0.0);
61 }
62
63 Ok((q / q_max).clamp(0.0, 1.0))
64}
65
66pub fn community_size_balance(graph: &Graph) -> IgraphResult<f64> {
84 let n = graph.vcount() as usize;
85 if n < 2 {
86 return Ok(0.0);
87 }
88
89 let m = graph.ecount();
90 if m == 0 {
91 return Ok(0.0);
92 }
93
94 let membership = greedy_communities(graph, n)?;
95 let k = *membership.iter().max().unwrap_or(&0) + 1;
96 if k <= 1 {
97 return Ok(0.0);
98 }
99
100 let mut sizes = vec![0_u64; k];
101 for &c in &membership {
102 sizes[c] += 1;
103 }
104
105 let n_f = n as f64;
106 let mut entropy = 0.0_f64;
107 for &s in &sizes {
108 if s > 0 {
109 let p = s as f64 / n_f;
110 entropy -= p * p.ln();
111 }
112 }
113
114 let max_entropy = (k as f64).ln();
115 if max_entropy < 1e-30 {
116 return Ok(0.0);
117 }
118
119 Ok(entropy / max_entropy)
120}
121
122pub fn inter_community_edge_ratio(graph: &Graph) -> IgraphResult<f64> {
139 let n = graph.vcount() as usize;
140 let m = graph.ecount();
141 if n < 2 || m == 0 {
142 return Ok(0.0);
143 }
144
145 let membership = greedy_communities(graph, n)?;
146
147 let mut inter_edges = 0_u64;
148 for v in 0..n {
149 let nbrs = graph.neighbors(v as u32)?;
150 for &u in &nbrs {
151 let ui = u as usize;
152 if ui > v && membership[v] != membership[ui] {
153 inter_edges += 1;
154 }
155 }
156 }
157
158 Ok(inter_edges as f64 / m as f64)
159}
160
161fn greedy_communities(graph: &Graph, n: usize) -> IgraphResult<Vec<usize>> {
165 let mut membership: Vec<usize> = (0..n).collect();
166
167 for _ in 0..10 {
168 let mut changed = false;
169 for v in 0..n {
170 let nbrs = graph.neighbors(v as u32)?;
171 if nbrs.is_empty() {
172 continue;
173 }
174
175 let mut freq: std::collections::HashMap<usize, usize> =
176 std::collections::HashMap::new();
177 for &u in &nbrs {
178 *freq.entry(membership[u as usize]).or_insert(0) += 1;
179 }
180
181 let best_community = freq
182 .into_iter()
183 .max_by_key(|&(_, count)| count)
184 .map_or(membership[v], |(comm, _)| comm);
185
186 if best_community != membership[v] {
187 membership[v] = best_community;
188 changed = true;
189 }
190 }
191 if !changed {
192 break;
193 }
194 }
195
196 let mut mapping: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
198 let mut next_id = 0_usize;
199 for v in 0..n {
200 let c = membership[v];
201 let new_id = *mapping.entry(c).or_insert_with(|| {
202 let id = next_id;
203 next_id += 1;
204 id
205 });
206 membership[v] = new_id;
207 }
208
209 Ok(membership)
210}
211
212fn compute_modularity(
214 graph: &Graph,
215 n: usize,
216 m: usize,
217 membership: &[usize],
218) -> IgraphResult<f64> {
219 if m == 0 {
220 return Ok(0.0);
221 }
222
223 let two_m = 2.0 * m as f64;
224 let mut degrees = Vec::with_capacity(n);
225 for v in 0..n {
226 degrees.push(graph.degree(v as u32)? as f64);
227 }
228
229 let k = *membership.iter().max().unwrap_or(&0) + 1;
230 let mut e_cc = vec![0.0_f64; k]; let mut a_c = vec![0.0_f64; k]; for v in 0..n {
234 let c = membership[v];
235 a_c[c] += degrees[v];
236 let nbrs = graph.neighbors(v as u32)?;
237 for &u in &nbrs {
238 let ui = u as usize;
239 if ui > v && membership[ui] == c {
240 e_cc[c] += 1.0;
241 }
242 }
243 }
244
245 let mut q = 0.0_f64;
246 for c in 0..k {
247 q += e_cc[c] / (m as f64) - (a_c[c] / two_m).powi(2);
248 }
249
250 Ok(q)
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 fn empty() -> Graph {
258 Graph::with_vertices(0)
259 }
260
261 fn single() -> Graph {
262 Graph::with_vertices(1)
263 }
264
265 fn single_edge() -> Graph {
266 Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
267 }
268
269 fn k3() -> Graph {
270 Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
271 }
272
273 fn k4() -> Graph {
274 Graph::from_edges(
275 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
276 false,
277 Some(4),
278 )
279 .unwrap()
280 }
281
282 fn cycle4() -> Graph {
283 Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
284 }
285
286 fn star5() -> Graph {
287 Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
288 }
289
290 fn two_triangles() -> Graph {
291 Graph::from_edges(
293 &[(0, 1), (0, 2), (1, 2), (2, 3), (3, 4), (3, 5), (4, 5)],
294 false,
295 Some(6),
296 )
297 .unwrap()
298 }
299
300 fn disconnected_k2s() -> Graph {
301 Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap()
302 }
303
304 #[test]
307 fn mubr_empty() {
308 assert!(modularity_upper_bound_ratio(&empty()).unwrap().abs() < 1e-10);
309 }
310
311 #[test]
312 fn mubr_single() {
313 assert!(modularity_upper_bound_ratio(&single()).unwrap().abs() < 1e-10);
314 }
315
316 #[test]
317 fn mubr_in_01() {
318 for g in &[
319 single_edge(),
320 k3(),
321 k4(),
322 cycle4(),
323 star5(),
324 two_triangles(),
325 ] {
326 let r = modularity_upper_bound_ratio(g).unwrap();
327 assert!(r >= -0.01);
328 assert!(r <= 1.01);
329 }
330 }
331
332 #[test]
333 fn mubr_disconnected_high() {
334 let r = modularity_upper_bound_ratio(&disconnected_k2s()).unwrap();
336 assert!(r > 0.5);
337 }
338
339 #[test]
340 fn mubr_finite() {
341 for g in &[
342 single_edge(),
343 k3(),
344 k4(),
345 cycle4(),
346 star5(),
347 two_triangles(),
348 ] {
349 assert!(modularity_upper_bound_ratio(g).unwrap().is_finite());
350 }
351 }
352
353 #[test]
356 fn csb_empty() {
357 assert!(community_size_balance(&empty()).unwrap().abs() < 1e-10);
358 }
359
360 #[test]
361 fn csb_single() {
362 assert!(community_size_balance(&single()).unwrap().abs() < 1e-10);
363 }
364
365 #[test]
366 fn csb_disconnected() {
367 let r = community_size_balance(&disconnected_k2s()).unwrap();
369 assert!((r - 1.0).abs() < 0.1);
370 }
371
372 #[test]
373 fn csb_in_01() {
374 for g in &[
375 single_edge(),
376 k3(),
377 k4(),
378 cycle4(),
379 star5(),
380 two_triangles(),
381 ] {
382 let r = community_size_balance(g).unwrap();
383 assert!(r >= -0.01);
384 assert!(r <= 1.01);
385 }
386 }
387
388 #[test]
389 fn csb_finite() {
390 for g in &[single_edge(), k3(), k4(), cycle4(), star5()] {
391 assert!(community_size_balance(g).unwrap().is_finite());
392 }
393 }
394
395 #[test]
398 fn icer_empty() {
399 assert!(inter_community_edge_ratio(&empty()).unwrap().abs() < 1e-10);
400 }
401
402 #[test]
403 fn icer_single() {
404 assert!(inter_community_edge_ratio(&single()).unwrap().abs() < 1e-10);
405 }
406
407 #[test]
408 fn icer_disconnected() {
409 assert!(
411 inter_community_edge_ratio(&disconnected_k2s())
412 .unwrap()
413 .abs()
414 < 1e-10
415 );
416 }
417
418 #[test]
419 fn icer_in_01() {
420 for g in &[
421 single_edge(),
422 k3(),
423 k4(),
424 cycle4(),
425 star5(),
426 two_triangles(),
427 ] {
428 let r = inter_community_edge_ratio(g).unwrap();
429 assert!(r >= -0.01);
430 assert!(r <= 1.01);
431 }
432 }
433
434 #[test]
435 fn icer_finite() {
436 for g in &[single_edge(), k3(), k4(), cycle4(), star5()] {
437 assert!(inter_community_edge_ratio(g).unwrap().is_finite());
438 }
439 }
440
441 #[test]
444 fn disconnected_strong_community() {
445 let g = disconnected_k2s();
446 assert!(modularity_upper_bound_ratio(&g).unwrap() > 0.5);
448 assert!(community_size_balance(&g).unwrap() > 0.8);
449 assert!(inter_community_edge_ratio(&g).unwrap() < 0.01);
450 }
451
452 #[test]
453 fn complete_weak_community() {
454 let r = inter_community_edge_ratio(&k4()).unwrap();
456 assert!(r.is_finite());
459 }
460}