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 let Some(id) = remap[slot] else {
89 return Err(IgraphError::Internal(
90 "membership reindex failed: missing label",
91 ));
92 };
93 reindexed.push(id);
94 }
95 let no_of_partitions = next_id as usize;
96
97 let mut k_part = vec![0.0_f64; no_of_partitions];
100 let mut e_internal = 0.0_f64; let m_u32 =
103 u32::try_from(ecount).map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
104 for eid in 0..m_u32 {
105 let (src, dst) = graph.edge(eid as EdgeId)?;
106 let cu = reindexed[src as usize];
107 let cv = reindexed[dst as usize];
108 if cu == cv {
109 e_internal += 2.0;
112 }
113 k_part[cu as usize] += 1.0;
117 k_part[cv as usize] += 1.0;
118 }
119
120 #[allow(clippy::cast_precision_loss)]
121 let m_f = ecount as f64;
122 let two_m = 2.0 * m_f;
123 for slot in &mut k_part {
125 *slot /= two_m;
126 }
127 let e_norm = e_internal / two_m;
128
129 let mut q = e_norm;
130 for &kc in &k_part {
131 q -= resolution * kc * kc;
132 }
133 Ok(Some(q))
134}
135
136pub fn modularity_weighted(
170 graph: &Graph,
171 membership: &[u32],
172 resolution: f64,
173 weights: &[f64],
174) -> IgraphResult<Option<f64>> {
175 if graph.is_directed() {
176 return Err(IgraphError::Unsupported(
177 "directed weighted modularity is ALGO-CO-001b/c follow-up; not yet ported",
178 ));
179 }
180 let n = graph.vcount() as usize;
181 if membership.len() != n {
182 return Err(IgraphError::InvalidArgument(
183 "membership vector size differs from number of vertices".to_string(),
184 ));
185 }
186 if !resolution.is_finite() || resolution < 0.0 {
187 return Err(IgraphError::InvalidArgument(
188 "resolution parameter must be non-negative and finite".to_string(),
189 ));
190 }
191 let ecount = graph.ecount();
192 if weights.len() != ecount {
193 return Err(IgraphError::InvalidArgument(format!(
194 "weights vector size ({}) differs from edge count ({})",
195 weights.len(),
196 ecount
197 )));
198 }
199 for (e, &w) in weights.iter().enumerate() {
200 if w.is_nan() {
201 return Err(IgraphError::InvalidArgument(format!(
202 "weight at edge {e} is NaN"
203 )));
204 }
205 if w < 0.0 {
206 return Err(IgraphError::InvalidArgument(format!(
207 "weight at edge {e} is negative ({w}); modularity is undefined"
208 )));
209 }
210 if !w.is_finite() {
211 return Err(IgraphError::InvalidArgument(format!(
212 "weight at edge {e} is not finite ({w})"
213 )));
214 }
215 }
216 if ecount == 0 {
217 return Ok(None);
218 }
219
220 let max_label = membership.iter().copied().max().unwrap_or(0);
222 let mut remap: Vec<Option<u32>> = vec![None; max_label as usize + 1];
223 let mut next_id: u32 = 0;
224 let mut reindexed: Vec<u32> = Vec::with_capacity(n);
225 for &lbl in membership {
226 let slot = lbl as usize;
227 if remap[slot].is_none() {
228 remap[slot] = Some(next_id);
229 next_id += 1;
230 }
231 let Some(id) = remap[slot] else {
232 return Err(IgraphError::Internal(
233 "membership reindex failed: missing label",
234 ));
235 };
236 reindexed.push(id);
237 }
238 let no_of_partitions = next_id as usize;
239
240 let mut s_part = vec![0.0_f64; no_of_partitions];
241 let mut w_internal = 0.0_f64;
242 let mut total_w = 0.0_f64;
243
244 let m_u32 =
245 u32::try_from(ecount).map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
246 for eid in 0..m_u32 {
247 let (src, tgt) = graph.edge(eid as EdgeId)?;
248 let w = weights[eid as usize];
249 let cu = reindexed[src as usize];
250 let cv = reindexed[tgt as usize];
251 if cu == cv {
252 w_internal += 2.0 * w;
255 }
256 s_part[cu as usize] += w;
260 s_part[cv as usize] += w;
261 total_w += w;
262 }
263
264 if total_w == 0.0 {
265 return Ok(None);
266 }
267
268 let two_w = 2.0 * total_w;
269 for slot in &mut s_part {
270 *slot /= two_w;
271 }
272 let e_norm = w_internal / two_w;
273
274 let mut q = e_norm;
275 for &sc in &s_part {
276 q -= resolution * sc * sc;
277 }
278 Ok(Some(q))
279}
280
281pub fn modularity_directed(
317 graph: &Graph,
318 membership: &[u32],
319 resolution: f64,
320) -> IgraphResult<Option<f64>> {
321 if !graph.is_directed() {
322 return modularity(graph, membership, resolution);
324 }
325 let n = graph.vcount() as usize;
326 if membership.len() != n {
327 return Err(IgraphError::InvalidArgument(
328 "membership vector size differs from number of vertices".to_string(),
329 ));
330 }
331 if !resolution.is_finite() || resolution < 0.0 {
332 return Err(IgraphError::InvalidArgument(
333 "resolution parameter must be non-negative and finite".to_string(),
334 ));
335 }
336 let ecount = graph.ecount();
337 if ecount == 0 {
338 return Ok(None);
339 }
340
341 let max_label = membership.iter().copied().max().unwrap_or(0);
343 let mut remap: Vec<Option<u32>> = vec![None; max_label as usize + 1];
344 let mut next_id: u32 = 0;
345 let mut reindexed: Vec<u32> = Vec::with_capacity(n);
346 for &lbl in membership {
347 let slot = lbl as usize;
348 if remap[slot].is_none() {
349 remap[slot] = Some(next_id);
350 next_id += 1;
351 }
352 let Some(id) = remap[slot] else {
353 return Err(IgraphError::Internal(
354 "membership reindex failed: missing label",
355 ));
356 };
357 reindexed.push(id);
358 }
359 let no_of_partitions = next_id as usize;
360
361 let mut k_out = vec![0.0_f64; no_of_partitions];
364 let mut k_in = vec![0.0_f64; no_of_partitions];
365 let mut e_internal = 0.0_f64;
366
367 let m_u32 =
368 u32::try_from(ecount).map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
369 for eid in 0..m_u32 {
370 let (src, tgt) = graph.edge(eid as EdgeId)?;
371 let cu = reindexed[src as usize];
372 let cv = reindexed[tgt as usize];
373 if cu == cv {
374 e_internal += 1.0;
375 }
376 k_out[cu as usize] += 1.0;
377 k_in[cv as usize] += 1.0;
378 }
379
380 #[allow(clippy::cast_precision_loss)]
381 let m_f = ecount as f64;
382 for slot in &mut k_out {
383 *slot /= m_f;
384 }
385 for slot in &mut k_in {
386 *slot /= m_f;
387 }
388 let e_norm = e_internal / m_f;
389
390 let mut q = e_norm;
391 for c in 0..no_of_partitions {
392 q -= resolution * k_out[c] * k_in[c];
393 }
394 Ok(Some(q))
395}
396
397pub fn modularity_weighted_directed(
413 graph: &Graph,
414 membership: &[u32],
415 resolution: f64,
416 weights: &[f64],
417) -> IgraphResult<Option<f64>> {
418 if !graph.is_directed() {
419 return modularity_weighted(graph, membership, resolution, weights);
420 }
421 let n = graph.vcount() as usize;
422 if membership.len() != n {
423 return Err(IgraphError::InvalidArgument(
424 "membership vector size differs from number of vertices".to_string(),
425 ));
426 }
427 if !resolution.is_finite() || resolution < 0.0 {
428 return Err(IgraphError::InvalidArgument(
429 "resolution parameter must be non-negative and finite".to_string(),
430 ));
431 }
432 let ecount = graph.ecount();
433 if weights.len() != ecount {
434 return Err(IgraphError::InvalidArgument(format!(
435 "weights vector size ({}) differs from edge count ({})",
436 weights.len(),
437 ecount
438 )));
439 }
440 for (e, &w) in weights.iter().enumerate() {
441 if w.is_nan() {
442 return Err(IgraphError::InvalidArgument(format!(
443 "weight at edge {e} is NaN"
444 )));
445 }
446 if w < 0.0 {
447 return Err(IgraphError::InvalidArgument(format!(
448 "weight at edge {e} is negative ({w}); modularity is undefined"
449 )));
450 }
451 if !w.is_finite() {
452 return Err(IgraphError::InvalidArgument(format!(
453 "weight at edge {e} is not finite ({w})"
454 )));
455 }
456 }
457 if ecount == 0 {
458 return Ok(None);
459 }
460
461 let max_label = membership.iter().copied().max().unwrap_or(0);
462 let mut remap: Vec<Option<u32>> = vec![None; max_label as usize + 1];
463 let mut next_id: u32 = 0;
464 let mut reindexed: Vec<u32> = Vec::with_capacity(n);
465 for &lbl in membership {
466 let slot = lbl as usize;
467 if remap[slot].is_none() {
468 remap[slot] = Some(next_id);
469 next_id += 1;
470 }
471 let Some(id) = remap[slot] else {
472 return Err(IgraphError::Internal(
473 "membership reindex failed: missing label",
474 ));
475 };
476 reindexed.push(id);
477 }
478 let no_of_partitions = next_id as usize;
479
480 let mut s_out = vec![0.0_f64; no_of_partitions];
481 let mut s_in = vec![0.0_f64; no_of_partitions];
482 let mut w_internal = 0.0_f64;
483 let mut total_w = 0.0_f64;
484
485 let m_u32 =
486 u32::try_from(ecount).map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
487 for eid in 0..m_u32 {
488 let (src, tgt) = graph.edge(eid as EdgeId)?;
489 let w = weights[eid as usize];
490 let cu = reindexed[src as usize];
491 let cv = reindexed[tgt as usize];
492 if cu == cv {
493 w_internal += w;
494 }
495 s_out[cu as usize] += w;
496 s_in[cv as usize] += w;
497 total_w += w;
498 }
499
500 if total_w == 0.0 {
501 return Ok(None);
502 }
503
504 for slot in &mut s_out {
505 *slot /= total_w;
506 }
507 for slot in &mut s_in {
508 *slot /= total_w;
509 }
510 let e_norm = w_internal / total_w;
511
512 let mut q = e_norm;
513 for c in 0..no_of_partitions {
514 q -= resolution * s_out[c] * s_in[c];
515 }
516 Ok(Some(q))
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522
523 fn close(actual: f64, expected: f64, tol: f64) {
524 assert!(
525 (actual - expected).abs() < tol,
526 "actual={actual} expected={expected}"
527 );
528 }
529
530 #[test]
531 fn empty_graph_yields_none() {
532 let g = Graph::with_vertices(0);
533 let q = modularity(&g, &[], 1.0).unwrap();
534 assert!(q.is_none());
535 }
536
537 #[test]
538 fn no_edges_yields_none() {
539 let g = Graph::with_vertices(3);
540 let q = modularity(&g, &[0, 0, 0], 1.0).unwrap();
541 assert!(q.is_none());
542 }
543
544 #[test]
545 fn single_partition_zero_for_well_separated_clusters() {
546 let mut g = Graph::with_vertices(6);
551 for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
552 g.add_edge(u, v).unwrap();
553 }
554 let q = modularity(&g, &[0; 6], 1.0).unwrap().unwrap();
555 close(q, 0.0, 1e-12);
556 }
557
558 #[test]
559 fn k3_union_k3_with_bridge_two_communities_high_q() {
560 let mut g = Graph::with_vertices(6);
563 for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
564 g.add_edge(u, v).unwrap();
565 }
566 let q = modularity(&g, &[0, 0, 0, 1, 1, 1], 1.0).unwrap().unwrap();
567 close(q, 6.0_f64 / 7.0 - 0.5, 1e-12);
581 }
582
583 #[test]
584 fn negative_q_for_singleton_cluster_in_dense_graph() {
585 let mut g = Graph::with_vertices(4);
587 for u in 0..4u32 {
588 for v in (u + 1)..4 {
589 g.add_edge(u, v).unwrap();
590 }
591 }
592 let q = modularity(&g, &[0, 1, 2, 3], 1.0).unwrap().unwrap();
593 close(q, -0.25, 1e-12);
596 }
597
598 #[test]
599 fn membership_size_mismatch_errors() {
600 let mut g = Graph::with_vertices(3);
601 g.add_edge(0, 1).unwrap();
602 assert!(modularity(&g, &[0, 0], 1.0).is_err());
603 }
604
605 #[test]
606 fn negative_resolution_errors() {
607 let mut g = Graph::with_vertices(3);
608 g.add_edge(0, 1).unwrap();
609 assert!(modularity(&g, &[0, 0, 0], -0.1).is_err());
610 }
611
612 #[test]
613 fn directed_returns_unsupported() {
614 let mut g = Graph::new(2, true).unwrap();
615 g.add_edge(0, 1).unwrap();
616 assert!(modularity(&g, &[0, 0], 1.0).is_err());
617 }
618
619 #[test]
620 fn non_consecutive_labels_reindex_correctly() {
621 let mut g = Graph::with_vertices(6);
625 for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
626 g.add_edge(u, v).unwrap();
627 }
628 let q1 = modularity(&g, &[0, 0, 0, 1, 1, 1], 1.0).unwrap().unwrap();
629 let q2 = modularity(&g, &[7, 7, 7, 42, 42, 42], 1.0)
630 .unwrap()
631 .unwrap();
632 close(q1, q2, 1e-12);
633 }
634
635 #[test]
636 fn resolution_zero_yields_density_term_only() {
637 let mut g = Graph::with_vertices(4);
639 for u in 0..4u32 {
640 for v in (u + 1)..4 {
641 g.add_edge(u, v).unwrap();
642 }
643 }
644 let q = modularity(&g, &[0, 0, 1, 1], 0.0).unwrap().unwrap();
647 close(q, 4.0 / 12.0, 1e-12);
648 }
649
650 #[test]
653 fn weighted_unit_weights_match_unweighted() {
654 let mut g = Graph::with_vertices(6);
656 for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
657 g.add_edge(u, v).unwrap();
658 }
659 let mem = [0, 0, 0, 1, 1, 1];
660 let weights = vec![1.0_f64; 7];
661 let qw = modularity_weighted(&g, &mem, 1.0, &weights)
662 .unwrap()
663 .unwrap();
664 let q = modularity(&g, &mem, 1.0).unwrap().unwrap();
665 close(qw, q, 1e-12);
666 }
667
668 #[test]
669 fn weighted_balanced_heavy_internal_edges_increase_q() {
670 let mut g = Graph::with_vertices(6);
676 for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
677 g.add_edge(u, v).unwrap();
678 }
679 let mem = [0, 0, 0, 1, 1, 1];
680 let weights = vec![10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 0.1];
681 let qw = modularity_weighted(&g, &mem, 1.0, &weights)
682 .unwrap()
683 .unwrap();
684 let q_unit = modularity(&g, &mem, 1.0).unwrap().unwrap();
685 assert!(
686 qw > q_unit,
687 "balanced-heavy Q ({qw}) should exceed unit-weight Q ({q_unit})"
688 );
689 }
690
691 #[test]
692 fn weighted_zero_total_weight_yields_none() {
693 let mut g = Graph::with_vertices(2);
694 g.add_edge(0, 1).unwrap();
695 let q = modularity_weighted(&g, &[0, 0], 1.0, &[0.0]).unwrap();
696 assert!(q.is_none());
697 }
698
699 #[test]
700 fn weighted_negative_weight_errors() {
701 let mut g = Graph::with_vertices(2);
702 g.add_edge(0, 1).unwrap();
703 assert!(modularity_weighted(&g, &[0, 0], 1.0, &[-1.0]).is_err());
704 }
705
706 #[test]
707 fn weighted_size_mismatch_errors() {
708 let mut g = Graph::with_vertices(2);
709 g.add_edge(0, 1).unwrap();
710 assert!(modularity_weighted(&g, &[0, 0], 1.0, &[1.0, 2.0]).is_err());
711 }
712
713 #[test]
714 fn weighted_directed_returns_unsupported() {
715 let mut g = Graph::new(2, true).unwrap();
716 g.add_edge(0, 1).unwrap();
717 assert!(modularity_weighted(&g, &[0, 0], 1.0, &[1.0]).is_err());
718 }
719
720 #[test]
723 fn directed_two_triangles_with_bridge_high_q() {
724 let mut g = Graph::new(6, true).unwrap();
740 for &(u, v) in &[(0u32, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3), (2, 3)] {
741 g.add_edge(u, v).unwrap();
742 }
743 let q = modularity_directed(&g, &[0, 0, 0, 1, 1, 1], 1.0)
744 .unwrap()
745 .unwrap();
746 close(q, 18.0 / 49.0, 1e-12);
747 }
748
749 #[test]
750 fn directed_undirected_graph_routes_to_undirected_formula() {
751 let mut g = Graph::with_vertices(6);
752 for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
753 g.add_edge(u, v).unwrap();
754 }
755 let mem = [0u32, 0, 0, 1, 1, 1];
756 let a = modularity(&g, &mem, 1.0).unwrap();
757 let b = modularity_directed(&g, &mem, 1.0).unwrap();
758 assert_eq!(a, b);
759 }
760
761 #[test]
762 fn directed_no_edges_yields_none() {
763 let g = Graph::new(3, true).unwrap();
764 let q = modularity_directed(&g, &[0, 0, 0], 1.0).unwrap();
765 assert!(q.is_none());
766 }
767
768 #[test]
769 fn directed_membership_size_mismatch_errors() {
770 let mut g = Graph::new(3, true).unwrap();
771 g.add_edge(0, 1).unwrap();
772 assert!(modularity_directed(&g, &[0, 0], 1.0).is_err());
773 }
774
775 #[test]
776 fn directed_negative_resolution_errors() {
777 let mut g = Graph::new(3, true).unwrap();
778 g.add_edge(0, 1).unwrap();
779 assert!(modularity_directed(&g, &[0, 0, 0], -0.1).is_err());
780 }
781
782 #[test]
783 fn directed_3_cycle_single_partition_zero() {
784 let mut g = Graph::new(3, true).unwrap();
789 g.add_edge(0, 1).unwrap();
790 g.add_edge(1, 2).unwrap();
791 g.add_edge(2, 0).unwrap();
792 let q = modularity_directed(&g, &[0, 0, 0], 1.0).unwrap().unwrap();
793 close(q, 0.0, 1e-12);
794 }
795
796 #[test]
797 fn weighted_two_disjoint_edges_q_eq_half() {
798 let mut g = Graph::with_vertices(4);
803 g.add_edge(0, 1).unwrap();
804 g.add_edge(2, 3).unwrap();
805 let q = modularity_weighted(&g, &[0, 0, 1, 1], 1.0, &[1.0, 1.0])
806 .unwrap()
807 .unwrap();
808 close(q, 0.5, 1e-12);
809 }
810}