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
382pub fn modularity_weighted_directed(
398 graph: &Graph,
399 membership: &[u32],
400 resolution: f64,
401 weights: &[f64],
402) -> IgraphResult<Option<f64>> {
403 if !graph.is_directed() {
404 return modularity_weighted(graph, membership, resolution, weights);
405 }
406 let n = graph.vcount() as usize;
407 if membership.len() != n {
408 return Err(IgraphError::InvalidArgument(
409 "membership vector size differs from number of vertices".to_string(),
410 ));
411 }
412 if !resolution.is_finite() || resolution < 0.0 {
413 return Err(IgraphError::InvalidArgument(
414 "resolution parameter must be non-negative and finite".to_string(),
415 ));
416 }
417 let ecount = graph.ecount();
418 if weights.len() != ecount {
419 return Err(IgraphError::InvalidArgument(format!(
420 "weights vector size ({}) differs from edge count ({})",
421 weights.len(),
422 ecount
423 )));
424 }
425 for (e, &w) in weights.iter().enumerate() {
426 if w.is_nan() {
427 return Err(IgraphError::InvalidArgument(format!(
428 "weight at edge {e} is NaN"
429 )));
430 }
431 if w < 0.0 {
432 return Err(IgraphError::InvalidArgument(format!(
433 "weight at edge {e} is negative ({w}); modularity is undefined"
434 )));
435 }
436 if !w.is_finite() {
437 return Err(IgraphError::InvalidArgument(format!(
438 "weight at edge {e} is not finite ({w})"
439 )));
440 }
441 }
442 if ecount == 0 {
443 return Ok(None);
444 }
445
446 let max_label = membership.iter().copied().max().unwrap_or(0);
447 let mut remap: Vec<Option<u32>> = vec![None; max_label as usize + 1];
448 let mut next_id: u32 = 0;
449 let mut reindexed: Vec<u32> = Vec::with_capacity(n);
450 for &lbl in membership {
451 let slot = lbl as usize;
452 if remap[slot].is_none() {
453 remap[slot] = Some(next_id);
454 next_id += 1;
455 }
456 reindexed.push(remap[slot].expect("just assigned"));
457 }
458 let no_of_partitions = next_id as usize;
459
460 let mut s_out = vec![0.0_f64; no_of_partitions];
461 let mut s_in = vec![0.0_f64; no_of_partitions];
462 let mut w_internal = 0.0_f64;
463 let mut total_w = 0.0_f64;
464
465 let m_u32 =
466 u32::try_from(ecount).map_err(|_| IgraphError::Internal("ecount exceeds u32::MAX"))?;
467 for eid in 0..m_u32 {
468 let (src, tgt) = graph.edge(eid as EdgeId)?;
469 let w = weights[eid as usize];
470 let cu = reindexed[src as usize];
471 let cv = reindexed[tgt as usize];
472 if cu == cv {
473 w_internal += w;
474 }
475 s_out[cu as usize] += w;
476 s_in[cv as usize] += w;
477 total_w += w;
478 }
479
480 if total_w == 0.0 {
481 return Ok(None);
482 }
483
484 for slot in &mut s_out {
485 *slot /= total_w;
486 }
487 for slot in &mut s_in {
488 *slot /= total_w;
489 }
490 let e_norm = w_internal / total_w;
491
492 let mut q = e_norm;
493 for c in 0..no_of_partitions {
494 q -= resolution * s_out[c] * s_in[c];
495 }
496 Ok(Some(q))
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502
503 fn close(actual: f64, expected: f64, tol: f64) {
504 assert!(
505 (actual - expected).abs() < tol,
506 "actual={actual} expected={expected}"
507 );
508 }
509
510 #[test]
511 fn empty_graph_yields_none() {
512 let g = Graph::with_vertices(0);
513 let q = modularity(&g, &[], 1.0).unwrap();
514 assert!(q.is_none());
515 }
516
517 #[test]
518 fn no_edges_yields_none() {
519 let g = Graph::with_vertices(3);
520 let q = modularity(&g, &[0, 0, 0], 1.0).unwrap();
521 assert!(q.is_none());
522 }
523
524 #[test]
525 fn single_partition_zero_for_well_separated_clusters() {
526 let mut g = Graph::with_vertices(6);
531 for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
532 g.add_edge(u, v).unwrap();
533 }
534 let q = modularity(&g, &[0; 6], 1.0).unwrap().unwrap();
535 close(q, 0.0, 1e-12);
536 }
537
538 #[test]
539 fn k3_union_k3_with_bridge_two_communities_high_q() {
540 let mut g = Graph::with_vertices(6);
543 for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
544 g.add_edge(u, v).unwrap();
545 }
546 let q = modularity(&g, &[0, 0, 0, 1, 1, 1], 1.0).unwrap().unwrap();
547 close(q, 6.0_f64 / 7.0 - 0.5, 1e-12);
561 }
562
563 #[test]
564 fn negative_q_for_singleton_cluster_in_dense_graph() {
565 let mut g = Graph::with_vertices(4);
567 for u in 0..4u32 {
568 for v in (u + 1)..4 {
569 g.add_edge(u, v).unwrap();
570 }
571 }
572 let q = modularity(&g, &[0, 1, 2, 3], 1.0).unwrap().unwrap();
573 close(q, -0.25, 1e-12);
576 }
577
578 #[test]
579 fn membership_size_mismatch_errors() {
580 let mut g = Graph::with_vertices(3);
581 g.add_edge(0, 1).unwrap();
582 assert!(modularity(&g, &[0, 0], 1.0).is_err());
583 }
584
585 #[test]
586 fn negative_resolution_errors() {
587 let mut g = Graph::with_vertices(3);
588 g.add_edge(0, 1).unwrap();
589 assert!(modularity(&g, &[0, 0, 0], -0.1).is_err());
590 }
591
592 #[test]
593 fn directed_returns_unsupported() {
594 let mut g = Graph::new(2, true).unwrap();
595 g.add_edge(0, 1).unwrap();
596 assert!(modularity(&g, &[0, 0], 1.0).is_err());
597 }
598
599 #[test]
600 fn non_consecutive_labels_reindex_correctly() {
601 let mut g = Graph::with_vertices(6);
605 for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
606 g.add_edge(u, v).unwrap();
607 }
608 let q1 = modularity(&g, &[0, 0, 0, 1, 1, 1], 1.0).unwrap().unwrap();
609 let q2 = modularity(&g, &[7, 7, 7, 42, 42, 42], 1.0)
610 .unwrap()
611 .unwrap();
612 close(q1, q2, 1e-12);
613 }
614
615 #[test]
616 fn resolution_zero_yields_density_term_only() {
617 let mut g = Graph::with_vertices(4);
619 for u in 0..4u32 {
620 for v in (u + 1)..4 {
621 g.add_edge(u, v).unwrap();
622 }
623 }
624 let q = modularity(&g, &[0, 0, 1, 1], 0.0).unwrap().unwrap();
627 close(q, 4.0 / 12.0, 1e-12);
628 }
629
630 #[test]
633 fn weighted_unit_weights_match_unweighted() {
634 let mut g = Graph::with_vertices(6);
636 for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
637 g.add_edge(u, v).unwrap();
638 }
639 let mem = [0, 0, 0, 1, 1, 1];
640 let weights = vec![1.0_f64; 7];
641 let qw = modularity_weighted(&g, &mem, 1.0, &weights)
642 .unwrap()
643 .unwrap();
644 let q = modularity(&g, &mem, 1.0).unwrap().unwrap();
645 close(qw, q, 1e-12);
646 }
647
648 #[test]
649 fn weighted_balanced_heavy_internal_edges_increase_q() {
650 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![10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 0.1];
661 let qw = modularity_weighted(&g, &mem, 1.0, &weights)
662 .unwrap()
663 .unwrap();
664 let q_unit = modularity(&g, &mem, 1.0).unwrap().unwrap();
665 assert!(
666 qw > q_unit,
667 "balanced-heavy Q ({qw}) should exceed unit-weight Q ({q_unit})"
668 );
669 }
670
671 #[test]
672 fn weighted_zero_total_weight_yields_none() {
673 let mut g = Graph::with_vertices(2);
674 g.add_edge(0, 1).unwrap();
675 let q = modularity_weighted(&g, &[0, 0], 1.0, &[0.0]).unwrap();
676 assert!(q.is_none());
677 }
678
679 #[test]
680 fn weighted_negative_weight_errors() {
681 let mut g = Graph::with_vertices(2);
682 g.add_edge(0, 1).unwrap();
683 assert!(modularity_weighted(&g, &[0, 0], 1.0, &[-1.0]).is_err());
684 }
685
686 #[test]
687 fn weighted_size_mismatch_errors() {
688 let mut g = Graph::with_vertices(2);
689 g.add_edge(0, 1).unwrap();
690 assert!(modularity_weighted(&g, &[0, 0], 1.0, &[1.0, 2.0]).is_err());
691 }
692
693 #[test]
694 fn weighted_directed_returns_unsupported() {
695 let mut g = Graph::new(2, true).unwrap();
696 g.add_edge(0, 1).unwrap();
697 assert!(modularity_weighted(&g, &[0, 0], 1.0, &[1.0]).is_err());
698 }
699
700 #[test]
703 fn directed_two_triangles_with_bridge_high_q() {
704 let mut g = Graph::new(6, true).unwrap();
720 for &(u, v) in &[(0u32, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3), (2, 3)] {
721 g.add_edge(u, v).unwrap();
722 }
723 let q = modularity_directed(&g, &[0, 0, 0, 1, 1, 1], 1.0)
724 .unwrap()
725 .unwrap();
726 close(q, 18.0 / 49.0, 1e-12);
727 }
728
729 #[test]
730 fn directed_undirected_graph_routes_to_undirected_formula() {
731 let mut g = Graph::with_vertices(6);
732 for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
733 g.add_edge(u, v).unwrap();
734 }
735 let mem = [0u32, 0, 0, 1, 1, 1];
736 let a = modularity(&g, &mem, 1.0).unwrap();
737 let b = modularity_directed(&g, &mem, 1.0).unwrap();
738 assert_eq!(a, b);
739 }
740
741 #[test]
742 fn directed_no_edges_yields_none() {
743 let g = Graph::new(3, true).unwrap();
744 let q = modularity_directed(&g, &[0, 0, 0], 1.0).unwrap();
745 assert!(q.is_none());
746 }
747
748 #[test]
749 fn directed_membership_size_mismatch_errors() {
750 let mut g = Graph::new(3, true).unwrap();
751 g.add_edge(0, 1).unwrap();
752 assert!(modularity_directed(&g, &[0, 0], 1.0).is_err());
753 }
754
755 #[test]
756 fn directed_negative_resolution_errors() {
757 let mut g = Graph::new(3, true).unwrap();
758 g.add_edge(0, 1).unwrap();
759 assert!(modularity_directed(&g, &[0, 0, 0], -0.1).is_err());
760 }
761
762 #[test]
763 fn directed_3_cycle_single_partition_zero() {
764 let mut g = Graph::new(3, true).unwrap();
769 g.add_edge(0, 1).unwrap();
770 g.add_edge(1, 2).unwrap();
771 g.add_edge(2, 0).unwrap();
772 let q = modularity_directed(&g, &[0, 0, 0], 1.0).unwrap().unwrap();
773 close(q, 0.0, 1e-12);
774 }
775
776 #[test]
777 fn weighted_two_disjoint_edges_q_eq_half() {
778 let mut g = Graph::with_vertices(4);
783 g.add_edge(0, 1).unwrap();
784 g.add_edge(2, 3).unwrap();
785 let q = modularity_weighted(&g, &[0, 0, 1, 1], 1.0, &[1.0, 1.0])
786 .unwrap()
787 .unwrap();
788 close(q, 0.5, 1e-12);
789 }
790}