1use crate::core::graph::EdgeId;
25use crate::core::{Graph, IgraphError, IgraphResult};
26
27pub fn modularity(graph: &Graph, membership: &[u32], resolution: f64) -> IgraphResult<Option<f64>> {
55 if graph.is_directed() {
56 return Err(IgraphError::Unsupported(
57 "directed modularity is ALGO-CO-001b; not yet ported",
58 ));
59 }
60 let n = graph.vcount() as usize;
61 if membership.len() != n {
62 return Err(IgraphError::InvalidArgument(
63 "membership vector size differs from number of vertices".to_string(),
64 ));
65 }
66 if !resolution.is_finite() || resolution < 0.0 {
67 return Err(IgraphError::InvalidArgument(
68 "resolution parameter must be non-negative and finite".to_string(),
69 ));
70 }
71
72 let ecount = graph.ecount();
73 if ecount == 0 {
74 return Ok(None);
75 }
76
77 let max_label = membership.iter().copied().max().unwrap_or(0);
79 let mut remap: Vec<Option<u32>> = vec![None; max_label as usize + 1];
80 let mut next_id: u32 = 0;
81 let mut reindexed: Vec<u32> = Vec::with_capacity(n);
82 for &lbl in membership {
83 let slot = lbl as usize;
84 if remap[slot].is_none() {
85 remap[slot] = Some(next_id);
86 next_id += 1;
87 }
88 reindexed.push(remap[slot].expect("just assigned"));
89 }
90 let no_of_partitions = next_id as usize;
91
92 let mut k_part = vec![0.0_f64; no_of_partitions];
95 let mut e_internal = 0.0_f64; let m_u32 =
98 u32::try_from(ecount).map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
99 for eid in 0..m_u32 {
100 let (src, dst) = graph.edge(eid as EdgeId)?;
101 let cu = reindexed[src as usize];
102 let cv = reindexed[dst as usize];
103 if cu == cv {
104 e_internal += 2.0;
107 }
108 k_part[cu as usize] += 1.0;
112 k_part[cv as usize] += 1.0;
113 }
114
115 #[allow(clippy::cast_precision_loss)]
116 let m_f = ecount as f64;
117 let two_m = 2.0 * m_f;
118 for slot in &mut k_part {
120 *slot /= two_m;
121 }
122 let e_norm = e_internal / two_m;
123
124 let mut q = e_norm;
125 for &kc in &k_part {
126 q -= resolution * kc * kc;
127 }
128 Ok(Some(q))
129}
130
131pub fn modularity_weighted(
165 graph: &Graph,
166 membership: &[u32],
167 resolution: f64,
168 weights: &[f64],
169) -> IgraphResult<Option<f64>> {
170 if graph.is_directed() {
171 return Err(IgraphError::Unsupported(
172 "directed weighted modularity is ALGO-CO-001b/c follow-up; not yet ported",
173 ));
174 }
175 let n = graph.vcount() as usize;
176 if membership.len() != n {
177 return Err(IgraphError::InvalidArgument(
178 "membership vector size differs from number of vertices".to_string(),
179 ));
180 }
181 if !resolution.is_finite() || resolution < 0.0 {
182 return Err(IgraphError::InvalidArgument(
183 "resolution parameter must be non-negative and finite".to_string(),
184 ));
185 }
186 let ecount = graph.ecount();
187 if weights.len() != ecount {
188 return Err(IgraphError::InvalidArgument(format!(
189 "weights vector size ({}) differs from edge count ({})",
190 weights.len(),
191 ecount
192 )));
193 }
194 for (e, &w) in weights.iter().enumerate() {
195 if w.is_nan() {
196 return Err(IgraphError::InvalidArgument(format!(
197 "weight at edge {e} is NaN"
198 )));
199 }
200 if w < 0.0 {
201 return Err(IgraphError::InvalidArgument(format!(
202 "weight at edge {e} is negative ({w}); modularity is undefined"
203 )));
204 }
205 if !w.is_finite() {
206 return Err(IgraphError::InvalidArgument(format!(
207 "weight at edge {e} is not finite ({w})"
208 )));
209 }
210 }
211 if ecount == 0 {
212 return Ok(None);
213 }
214
215 let max_label = membership.iter().copied().max().unwrap_or(0);
217 let mut remap: Vec<Option<u32>> = vec![None; max_label as usize + 1];
218 let mut next_id: u32 = 0;
219 let mut reindexed: Vec<u32> = Vec::with_capacity(n);
220 for &lbl in membership {
221 let slot = lbl as usize;
222 if remap[slot].is_none() {
223 remap[slot] = Some(next_id);
224 next_id += 1;
225 }
226 reindexed.push(remap[slot].expect("just assigned"));
227 }
228 let no_of_partitions = next_id as usize;
229
230 let mut s_part = vec![0.0_f64; no_of_partitions];
231 let mut w_internal = 0.0_f64;
232 let mut total_w = 0.0_f64;
233
234 let m_u32 =
235 u32::try_from(ecount).map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
236 for eid in 0..m_u32 {
237 let (src, tgt) = graph.edge(eid as EdgeId)?;
238 let w = weights[eid as usize];
239 let cu = reindexed[src as usize];
240 let cv = reindexed[tgt as usize];
241 if cu == cv {
242 w_internal += 2.0 * w;
245 }
246 s_part[cu as usize] += w;
250 s_part[cv as usize] += w;
251 total_w += w;
252 }
253
254 if total_w == 0.0 {
255 return Ok(None);
256 }
257
258 let two_w = 2.0 * total_w;
259 for slot in &mut s_part {
260 *slot /= two_w;
261 }
262 let e_norm = w_internal / two_w;
263
264 let mut q = e_norm;
265 for &sc in &s_part {
266 q -= resolution * sc * sc;
267 }
268 Ok(Some(q))
269}
270
271pub fn modularity_directed(
307 graph: &Graph,
308 membership: &[u32],
309 resolution: f64,
310) -> IgraphResult<Option<f64>> {
311 if !graph.is_directed() {
312 return modularity(graph, membership, resolution);
314 }
315 let n = graph.vcount() as usize;
316 if membership.len() != n {
317 return Err(IgraphError::InvalidArgument(
318 "membership vector size differs from number of vertices".to_string(),
319 ));
320 }
321 if !resolution.is_finite() || resolution < 0.0 {
322 return Err(IgraphError::InvalidArgument(
323 "resolution parameter must be non-negative and finite".to_string(),
324 ));
325 }
326 let ecount = graph.ecount();
327 if ecount == 0 {
328 return Ok(None);
329 }
330
331 let max_label = membership.iter().copied().max().unwrap_or(0);
333 let mut remap: Vec<Option<u32>> = vec![None; max_label as usize + 1];
334 let mut next_id: u32 = 0;
335 let mut reindexed: Vec<u32> = Vec::with_capacity(n);
336 for &lbl in membership {
337 let slot = lbl as usize;
338 if remap[slot].is_none() {
339 remap[slot] = Some(next_id);
340 next_id += 1;
341 }
342 reindexed.push(remap[slot].expect("just assigned"));
343 }
344 let no_of_partitions = next_id as usize;
345
346 let mut k_out = vec![0.0_f64; no_of_partitions];
349 let mut k_in = vec![0.0_f64; no_of_partitions];
350 let mut e_internal = 0.0_f64;
351
352 let m_u32 =
353 u32::try_from(ecount).map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
354 for eid in 0..m_u32 {
355 let (src, tgt) = graph.edge(eid as EdgeId)?;
356 let cu = reindexed[src as usize];
357 let cv = reindexed[tgt as usize];
358 if cu == cv {
359 e_internal += 1.0;
360 }
361 k_out[cu as usize] += 1.0;
362 k_in[cv as usize] += 1.0;
363 }
364
365 #[allow(clippy::cast_precision_loss)]
366 let m_f = ecount as f64;
367 for slot in &mut k_out {
368 *slot /= m_f;
369 }
370 for slot in &mut k_in {
371 *slot /= m_f;
372 }
373 let e_norm = e_internal / m_f;
374
375 let mut q = e_norm;
376 for c in 0..no_of_partitions {
377 q -= resolution * k_out[c] * k_in[c];
378 }
379 Ok(Some(q))
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385
386 fn close(actual: f64, expected: f64, tol: f64) {
387 assert!(
388 (actual - expected).abs() < tol,
389 "actual={actual} expected={expected}"
390 );
391 }
392
393 #[test]
394 fn empty_graph_yields_none() {
395 let g = Graph::with_vertices(0);
396 let q = modularity(&g, &[], 1.0).unwrap();
397 assert!(q.is_none());
398 }
399
400 #[test]
401 fn no_edges_yields_none() {
402 let g = Graph::with_vertices(3);
403 let q = modularity(&g, &[0, 0, 0], 1.0).unwrap();
404 assert!(q.is_none());
405 }
406
407 #[test]
408 fn single_partition_zero_for_well_separated_clusters() {
409 let mut g = Graph::with_vertices(6);
414 for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
415 g.add_edge(u, v).unwrap();
416 }
417 let q = modularity(&g, &[0; 6], 1.0).unwrap().unwrap();
418 close(q, 0.0, 1e-12);
419 }
420
421 #[test]
422 fn k3_union_k3_with_bridge_two_communities_high_q() {
423 let mut g = Graph::with_vertices(6);
426 for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
427 g.add_edge(u, v).unwrap();
428 }
429 let q = modularity(&g, &[0, 0, 0, 1, 1, 1], 1.0).unwrap().unwrap();
430 close(q, 6.0_f64 / 7.0 - 0.5, 1e-12);
444 }
445
446 #[test]
447 fn negative_q_for_singleton_cluster_in_dense_graph() {
448 let mut g = Graph::with_vertices(4);
450 for u in 0..4u32 {
451 for v in (u + 1)..4 {
452 g.add_edge(u, v).unwrap();
453 }
454 }
455 let q = modularity(&g, &[0, 1, 2, 3], 1.0).unwrap().unwrap();
456 close(q, -0.25, 1e-12);
459 }
460
461 #[test]
462 fn membership_size_mismatch_errors() {
463 let mut g = Graph::with_vertices(3);
464 g.add_edge(0, 1).unwrap();
465 assert!(modularity(&g, &[0, 0], 1.0).is_err());
466 }
467
468 #[test]
469 fn negative_resolution_errors() {
470 let mut g = Graph::with_vertices(3);
471 g.add_edge(0, 1).unwrap();
472 assert!(modularity(&g, &[0, 0, 0], -0.1).is_err());
473 }
474
475 #[test]
476 fn directed_returns_unsupported() {
477 let mut g = Graph::new(2, true).unwrap();
478 g.add_edge(0, 1).unwrap();
479 assert!(modularity(&g, &[0, 0], 1.0).is_err());
480 }
481
482 #[test]
483 fn non_consecutive_labels_reindex_correctly() {
484 let mut g = Graph::with_vertices(6);
488 for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
489 g.add_edge(u, v).unwrap();
490 }
491 let q1 = modularity(&g, &[0, 0, 0, 1, 1, 1], 1.0).unwrap().unwrap();
492 let q2 = modularity(&g, &[7, 7, 7, 42, 42, 42], 1.0)
493 .unwrap()
494 .unwrap();
495 close(q1, q2, 1e-12);
496 }
497
498 #[test]
499 fn resolution_zero_yields_density_term_only() {
500 let mut g = Graph::with_vertices(4);
502 for u in 0..4u32 {
503 for v in (u + 1)..4 {
504 g.add_edge(u, v).unwrap();
505 }
506 }
507 let q = modularity(&g, &[0, 0, 1, 1], 0.0).unwrap().unwrap();
510 close(q, 4.0 / 12.0, 1e-12);
511 }
512
513 #[test]
516 fn weighted_unit_weights_match_unweighted() {
517 let mut g = Graph::with_vertices(6);
519 for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
520 g.add_edge(u, v).unwrap();
521 }
522 let mem = [0, 0, 0, 1, 1, 1];
523 let weights = vec![1.0_f64; 7];
524 let qw = modularity_weighted(&g, &mem, 1.0, &weights)
525 .unwrap()
526 .unwrap();
527 let q = modularity(&g, &mem, 1.0).unwrap().unwrap();
528 close(qw, q, 1e-12);
529 }
530
531 #[test]
532 fn weighted_balanced_heavy_internal_edges_increase_q() {
533 let mut g = Graph::with_vertices(6);
539 for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
540 g.add_edge(u, v).unwrap();
541 }
542 let mem = [0, 0, 0, 1, 1, 1];
543 let weights = vec![10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 0.1];
544 let qw = modularity_weighted(&g, &mem, 1.0, &weights)
545 .unwrap()
546 .unwrap();
547 let q_unit = modularity(&g, &mem, 1.0).unwrap().unwrap();
548 assert!(
549 qw > q_unit,
550 "balanced-heavy Q ({qw}) should exceed unit-weight Q ({q_unit})"
551 );
552 }
553
554 #[test]
555 fn weighted_zero_total_weight_yields_none() {
556 let mut g = Graph::with_vertices(2);
557 g.add_edge(0, 1).unwrap();
558 let q = modularity_weighted(&g, &[0, 0], 1.0, &[0.0]).unwrap();
559 assert!(q.is_none());
560 }
561
562 #[test]
563 fn weighted_negative_weight_errors() {
564 let mut g = Graph::with_vertices(2);
565 g.add_edge(0, 1).unwrap();
566 assert!(modularity_weighted(&g, &[0, 0], 1.0, &[-1.0]).is_err());
567 }
568
569 #[test]
570 fn weighted_size_mismatch_errors() {
571 let mut g = Graph::with_vertices(2);
572 g.add_edge(0, 1).unwrap();
573 assert!(modularity_weighted(&g, &[0, 0], 1.0, &[1.0, 2.0]).is_err());
574 }
575
576 #[test]
577 fn weighted_directed_returns_unsupported() {
578 let mut g = Graph::new(2, true).unwrap();
579 g.add_edge(0, 1).unwrap();
580 assert!(modularity_weighted(&g, &[0, 0], 1.0, &[1.0]).is_err());
581 }
582
583 #[test]
586 fn directed_two_triangles_with_bridge_high_q() {
587 let mut g = Graph::new(6, true).unwrap();
603 for &(u, v) in &[(0u32, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3), (2, 3)] {
604 g.add_edge(u, v).unwrap();
605 }
606 let q = modularity_directed(&g, &[0, 0, 0, 1, 1, 1], 1.0)
607 .unwrap()
608 .unwrap();
609 close(q, 18.0 / 49.0, 1e-12);
610 }
611
612 #[test]
613 fn directed_undirected_graph_routes_to_undirected_formula() {
614 let mut g = Graph::with_vertices(6);
615 for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
616 g.add_edge(u, v).unwrap();
617 }
618 let mem = [0u32, 0, 0, 1, 1, 1];
619 let a = modularity(&g, &mem, 1.0).unwrap();
620 let b = modularity_directed(&g, &mem, 1.0).unwrap();
621 assert_eq!(a, b);
622 }
623
624 #[test]
625 fn directed_no_edges_yields_none() {
626 let g = Graph::new(3, true).unwrap();
627 let q = modularity_directed(&g, &[0, 0, 0], 1.0).unwrap();
628 assert!(q.is_none());
629 }
630
631 #[test]
632 fn directed_membership_size_mismatch_errors() {
633 let mut g = Graph::new(3, true).unwrap();
634 g.add_edge(0, 1).unwrap();
635 assert!(modularity_directed(&g, &[0, 0], 1.0).is_err());
636 }
637
638 #[test]
639 fn directed_negative_resolution_errors() {
640 let mut g = Graph::new(3, true).unwrap();
641 g.add_edge(0, 1).unwrap();
642 assert!(modularity_directed(&g, &[0, 0, 0], -0.1).is_err());
643 }
644
645 #[test]
646 fn directed_3_cycle_single_partition_zero() {
647 let mut g = Graph::new(3, true).unwrap();
652 g.add_edge(0, 1).unwrap();
653 g.add_edge(1, 2).unwrap();
654 g.add_edge(2, 0).unwrap();
655 let q = modularity_directed(&g, &[0, 0, 0], 1.0).unwrap().unwrap();
656 close(q, 0.0, 1e-12);
657 }
658
659 #[test]
660 fn weighted_two_disjoint_edges_q_eq_half() {
661 let mut g = Graph::with_vertices(4);
666 g.add_edge(0, 1).unwrap();
667 g.add_edge(2, 3).unwrap();
668 let q = modularity_weighted(&g, &[0, 0, 1, 1], 1.0, &[1.0, 1.0])
669 .unwrap()
670 .unwrap();
671 close(q, 0.5, 1e-12);
672 }
673}