1use core_query::Dir;
33use core_storage::v8::seam::TopologyView;
34use core_storage::{Direction, EdgePropsView, IdMap, Interner, Value};
35use serde::{Deserialize, Serialize};
36use std::collections::BTreeMap;
37use std::time::{Duration, Instant};
38
39fn live_nodes(idmap: &IdMap, labels: &[u32]) -> (Vec<u32>, Vec<String>) {
48 let n = idmap.len() as u32;
49 let mut ids = Vec::new();
50 let mut keys = Vec::new();
51 for id in 0..n {
52 let Some(key) = idmap.key_of(id) else {
53 continue;
54 };
55 let Some(&sym) = labels.get(id as usize) else {
56 continue;
57 };
58 if sym == u32::MAX {
59 continue; }
61 ids.push(id);
62 keys.push(key.to_string());
63 }
64 (ids, keys)
65}
66
67fn resolve_etype(syms: &Interner, edge_type: Option<&str>) -> Option<Option<u32>> {
73 match edge_type {
74 None => Some(None), Some(name) => {
76 let sym = syms.get(name)?; Some(Some(sym))
78 }
79 }
80}
81
82fn etypes_filtered(topo: &TopologyView, filter: Option<u32>) -> Vec<u32> {
84 match filter {
85 Some(sym) => {
86 let all: Vec<u32> = topo.etypes().collect();
88 if all.contains(&sym) {
89 vec![sym]
90 } else {
91 vec![]
92 }
93 }
94 None => topo.etypes().collect(),
95 }
96}
97
98fn resolve_etypes_multi(syms: &Interner, topo: &TopologyView, names: &[String]) -> Vec<u32> {
102 if names.is_empty() {
103 return topo.etypes().collect();
104 }
105 let mut out = Vec::new();
106 for name in names {
107 if let Some(sym) = syms.get(name) {
108 if !out.contains(&sym) {
109 out.push(sym);
110 }
111 }
112 }
113 out
114}
115
116fn live_nodes_for_label(
120 idmap: &IdMap,
121 syms: &Interner,
122 labels: &[u32],
123 label: Option<&str>,
124) -> (Vec<u32>, Vec<String>) {
125 let want = match label {
126 None => None,
127 Some(name) => match syms.get(name) {
128 Some(sym) => Some(sym),
129 None => return (Vec::new(), Vec::new()),
130 },
131 };
132 let n = idmap.len() as u32;
133 let mut ids = Vec::new();
134 let mut keys = Vec::new();
135 for id in 0..n {
136 let Some(key) = idmap.key_of(id) else {
137 continue;
138 };
139 let Some(&sym) = labels.get(id as usize) else {
140 continue;
141 };
142 if sym == u32::MAX {
143 continue; }
145 if let Some(want_sym) = want {
146 if sym != want_sym {
147 continue;
148 }
149 }
150 ids.push(id);
151 keys.push(key.to_string());
152 }
153 (ids, keys)
154}
155
156fn edge_weight(
163 edge_props: &EdgePropsView,
164 etype: u32,
165 src: u32,
166 dst: u32,
167 weight_prop: Option<&str>,
168 min_weight: Option<f64>,
169) -> Option<f64> {
170 let w = match weight_prop {
171 None => 1.0,
172 Some(prop) => match edge_props.get(etype, src, dst, prop) {
173 Some(Value::Float(f)) => f,
174 Some(Value::Int(i)) => i as f64,
175 _ => 1.0,
176 },
177 };
178 match min_weight {
179 Some(min) if w < min => None,
180 _ => Some(w),
181 }
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
190#[serde(default)]
191pub struct PageRankConfig {
192 pub damping: f64,
195 pub max_iters: u32,
197 pub tol: f64,
199 pub edge_type: Option<String>,
201 pub direction: AlgoDir,
205 pub budget_ms: u64,
208 pub weight_prop: Option<String>,
212 pub min_weight: Option<f64>,
215}
216
217impl Default for PageRankConfig {
218 fn default() -> Self {
219 Self {
220 damping: 0.85,
221 max_iters: 50,
222 tol: 1e-6,
223 edge_type: None,
224 direction: AlgoDir::Out,
225 budget_ms: 5_000,
226 weight_prop: None,
227 min_weight: None,
228 }
229 }
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct PageRankReport {
235 pub scores: Vec<(String, f64)>,
238 pub converged: bool,
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
246#[serde(rename_all = "lowercase")]
247pub enum AlgoDir {
248 Out,
250 In,
252 Both,
255}
256
257impl From<Dir> for AlgoDir {
258 fn from(d: Dir) -> Self {
259 match d {
260 Dir::Out => AlgoDir::Out,
261 Dir::In => AlgoDir::In,
262 Dir::Both => AlgoDir::Both,
263 }
264 }
265}
266
267pub(crate) fn pagerank(
271 topo: &TopologyView,
272 idmap: &IdMap,
273 syms: &Interner,
274 labels: &[u32],
275 edge_props: &EdgePropsView,
276 config: &PageRankConfig,
277) -> PageRankReport {
278 let weighted = config.weight_prop.is_some() || config.min_weight.is_some();
279 let deadline = if config.budget_ms > 0 {
280 Some(Instant::now() + Duration::from_millis(config.budget_ms))
281 } else {
282 None
283 };
284
285 let (node_ids, node_keys) = live_nodes(idmap, labels);
286 let n = node_ids.len();
287
288 if n == 0 {
289 return PageRankReport {
290 scores: Vec::new(),
291 converged: true,
292 };
293 }
294
295 let max_id = topo.etypes().count(); let _ = max_id;
298 let mut id_to_idx: BTreeMap<u32, usize> = BTreeMap::new();
299 for (i, &id) in node_ids.iter().enumerate() {
300 id_to_idx.insert(id, i);
301 }
302
303 let etype_filter = match resolve_etype(syms, config.edge_type.as_deref()) {
305 None => {
306 let score = 1.0 / n as f64;
308 let mut scores: Vec<(String, f64)> =
309 node_keys.iter().map(|k| (k.clone(), score)).collect();
310 scores.sort_by(|(ka, sa), (kb, sb)| {
311 sb.partial_cmp(sa)
312 .unwrap_or(std::cmp::Ordering::Equal)
313 .then(ka.cmp(kb))
314 });
315 return PageRankReport {
316 scores,
317 converged: true,
318 };
319 }
320 Some(f) => f,
321 };
322
323 let etypes = etypes_filtered(topo, etype_filter);
324
325 let mut send_to: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
332
333 for &et in &etypes {
334 for (i, &id) in node_ids.iter().enumerate() {
335 let dirs: &[Direction] = match config.direction {
336 AlgoDir::Out => &[Direction::Out],
337 AlgoDir::In => &[Direction::In],
338 AlgoDir::Both => &[Direction::Out, Direction::In],
339 };
340 for &dir in dirs {
341 for &nbr in topo.neighbors(et, dir, id).as_ref() {
342 let Some(&j) = id_to_idx.get(&nbr) else {
343 continue;
344 };
345 if weighted {
346 let Some(w) = edge_weight(
347 edge_props,
348 et,
349 id,
350 nbr,
351 config.weight_prop.as_deref(),
352 config.min_weight,
353 ) else {
354 continue; };
356 if let Some(entry) = send_to[i].iter_mut().find(|(k, _)| *k == j) {
357 entry.1 += w;
358 } else {
359 send_to[i].push((j, w));
360 }
361 } else if !send_to[i].iter().any(|(k, _)| *k == j) {
362 send_to[i].push((j, 1.0));
363 }
364 }
365 }
366 }
367 }
368
369 let mut receive_from: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
373 let mut dangling: Vec<usize> = Vec::new();
374
375 for (i, send) in send_to.iter().enumerate() {
376 let out_weight: f64 = send.iter().map(|(_, w)| w).sum();
377 if send.is_empty() || out_weight <= 0.0 {
378 dangling.push(i);
379 } else {
380 for &(j, w) in send {
381 receive_from[j].push((i, w / out_weight));
382 }
383 }
384 }
385
386 let nf = n as f64;
388 let d = config.damping;
389 let teleport = (1.0 - d) / nf;
390 let mut pr: Vec<f64> = vec![1.0 / nf; n];
391 let mut converged = false;
392
393 for _iter in 0..config.max_iters {
394 if let Some(dl) = deadline {
396 if Instant::now() >= dl {
397 break;
398 }
399 }
400
401 let dangling_sum: f64 = dangling.iter().map(|&i| pr[i]).sum::<f64>() * d / nf;
403
404 let mut new_pr = vec![teleport + dangling_sum; n];
405 for j in 0..n {
406 let received: f64 = receive_from[j].iter().map(|&(i, w)| pr[i] * w).sum();
407 new_pr[j] += d * received;
408 }
409
410 let delta: f64 = pr
412 .iter()
413 .zip(new_pr.iter())
414 .map(|(a, b)| (a - b).abs())
415 .sum();
416 pr = new_pr;
417
418 if delta < config.tol {
419 converged = true;
420 break;
421 }
422 }
423
424 let mut scores: Vec<(String, f64)> = node_keys.into_iter().zip(pr).collect();
426 scores.sort_by(|(ka, sa), (kb, sb)| {
427 sb.partial_cmp(sa)
428 .unwrap_or(std::cmp::Ordering::Equal)
429 .then(ka.cmp(kb))
430 });
431
432 PageRankReport { scores, converged }
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize)]
441#[serde(default)]
442pub struct WccConfig {
443 pub edge_type: Option<String>,
445 pub budget_ms: u64,
447 pub weight_prop: Option<String>,
451 pub min_weight: Option<f64>,
454}
455
456impl Default for WccConfig {
457 fn default() -> Self {
458 Self {
459 edge_type: None,
460 budget_ms: 5_000,
461 weight_prop: None,
462 min_weight: None,
463 }
464 }
465}
466
467#[derive(Debug, Clone, Serialize, Deserialize)]
469pub struct WccReport {
470 pub components: Vec<(String, String)>,
473 pub truncated: bool,
475}
476
477struct UnionFind {
479 parent: Vec<usize>,
480 rank: Vec<u8>,
481}
482
483impl UnionFind {
484 fn new(n: usize) -> Self {
485 Self {
486 parent: (0..n).collect(),
487 rank: vec![0; n],
488 }
489 }
490
491 fn find(&mut self, mut x: usize) -> usize {
492 while self.parent[x] != x {
493 self.parent[x] = self.parent[self.parent[x]]; x = self.parent[x];
495 }
496 x
497 }
498
499 fn union(&mut self, a: usize, b: usize) {
500 let ra = self.find(a);
501 let rb = self.find(b);
502 if ra == rb {
503 return;
504 }
505 match self.rank[ra].cmp(&self.rank[rb]) {
506 std::cmp::Ordering::Less => self.parent[ra] = rb,
507 std::cmp::Ordering::Greater => self.parent[rb] = ra,
508 std::cmp::Ordering::Equal => {
509 self.parent[rb] = ra;
510 self.rank[ra] += 1;
511 }
512 }
513 }
514}
515
516pub(crate) fn wcc(
520 topo: &TopologyView,
521 idmap: &IdMap,
522 syms: &Interner,
523 labels: &[u32],
524 edge_props: &EdgePropsView,
525 config: &WccConfig,
526) -> WccReport {
527 let weighted = config.weight_prop.is_some() || config.min_weight.is_some();
528 let deadline = if config.budget_ms > 0 {
529 Some(Instant::now() + Duration::from_millis(config.budget_ms))
530 } else {
531 None
532 };
533
534 let (node_ids, node_keys) = live_nodes(idmap, labels);
535 let n = node_ids.len();
536
537 if n == 0 {
538 return WccReport {
539 components: Vec::new(),
540 truncated: false,
541 };
542 }
543
544 let mut id_to_idx: BTreeMap<u32, usize> = BTreeMap::new();
546 for (i, &id) in node_ids.iter().enumerate() {
547 id_to_idx.insert(id, i);
548 }
549
550 let etype_filter = match resolve_etype(syms, config.edge_type.as_deref()) {
552 None => {
553 let mut components: Vec<(String, String)> =
555 node_keys.iter().map(|k| (k.clone(), k.clone())).collect();
556 components.sort();
557 return WccReport {
558 components,
559 truncated: false,
560 };
561 }
562 Some(f) => f,
563 };
564
565 let etypes = etypes_filtered(topo, etype_filter);
566
567 let mut uf = UnionFind::new(n);
568 let mut truncated = false;
569
570 'outer: for &et in &etypes {
572 for (i, &id) in node_ids.iter().enumerate() {
573 if let Some(dl) = deadline {
574 if Instant::now() >= dl {
575 truncated = true;
576 break 'outer;
577 }
578 }
579 for &nbr in topo.neighbors(et, Direction::Out, id).as_ref() {
581 if let Some(&j) = id_to_idx.get(&nbr) {
582 if weighted
583 && edge_weight(
584 edge_props,
585 et,
586 id,
587 nbr,
588 config.weight_prop.as_deref(),
589 config.min_weight,
590 )
591 .is_none()
592 {
593 continue; }
595 uf.union(i, j);
596 }
597 }
598 for &nbr in topo.neighbors(et, Direction::In, id).as_ref() {
602 if let Some(&j) = id_to_idx.get(&nbr) {
603 if weighted
604 && edge_weight(
605 edge_props,
606 et,
607 nbr,
608 id,
609 config.weight_prop.as_deref(),
610 config.min_weight,
611 )
612 .is_none()
613 {
614 continue; }
616 uf.union(i, j);
617 }
618 }
619 }
620 }
621
622 let mut root_min_key: BTreeMap<usize, &str> = BTreeMap::new();
624 for (i, key_str) in node_keys.iter().enumerate() {
625 let root = uf.find(i);
626 let key = key_str.as_str();
627 let entry = root_min_key.entry(root).or_insert(key);
628 if key < *entry {
629 *entry = key;
630 }
631 }
632
633 let mut components: Vec<(String, String)> = node_keys
634 .iter()
635 .enumerate()
636 .map(|(i, key_str)| {
637 let root = uf.find(i);
638 let comp_id = root_min_key[&root].to_string();
639 (key_str.clone(), comp_id)
640 })
641 .collect();
642 components.sort_by(|(ka, ca), (kb, cb)| ca.cmp(cb).then(ka.cmp(kb)));
643
644 WccReport {
645 components,
646 truncated,
647 }
648}
649
650#[derive(Debug, Clone, Serialize, Deserialize)]
656#[serde(default)]
657pub struct DegreeConfig {
658 pub edge_type: Option<String>,
660 pub direction: AlgoDir,
662 pub budget_ms: u64,
664 pub weight_prop: Option<String>,
669 pub min_weight: Option<f64>,
672}
673
674impl Default for DegreeConfig {
675 fn default() -> Self {
676 Self {
677 edge_type: None,
678 direction: AlgoDir::Both,
679 budget_ms: 5_000,
680 weight_prop: None,
681 min_weight: None,
682 }
683 }
684}
685
686#[derive(Debug, Clone, Serialize, Deserialize)]
688pub struct DegreeReport {
689 pub scores: Vec<(String, u64)>,
691 pub truncated: bool,
693}
694
695pub(crate) fn degree_centrality(
697 topo: &TopologyView,
698 idmap: &IdMap,
699 syms: &Interner,
700 labels: &[u32],
701 edge_props: &EdgePropsView,
702 config: &DegreeConfig,
703) -> DegreeReport {
704 let weighted = config.weight_prop.is_some() || config.min_weight.is_some();
705 let deadline = if config.budget_ms > 0 {
706 Some(Instant::now() + Duration::from_millis(config.budget_ms))
707 } else {
708 None
709 };
710
711 let (node_ids, node_keys) = live_nodes(idmap, labels);
712 let n = node_ids.len();
713
714 if n == 0 {
715 return DegreeReport {
716 scores: Vec::new(),
717 truncated: false,
718 };
719 }
720
721 let etype_filter = match resolve_etype(syms, config.edge_type.as_deref()) {
723 None => {
724 let scores = node_keys.iter().map(|k| (k.clone(), 0u64)).collect();
726 return DegreeReport {
727 scores,
728 truncated: false,
729 };
730 }
731 Some(f) => f,
732 };
733
734 let etypes = etypes_filtered(topo, etype_filter);
735 let mut degrees: Vec<u64> = vec![0u64; n];
736 let mut truncated = false;
737
738 for (i, &id) in node_ids.iter().enumerate() {
739 if let Some(dl) = deadline {
740 if Instant::now() >= dl {
741 truncated = true;
742 break;
743 }
744 }
745 for &et in &etypes {
746 let dirs: &[Direction] = match config.direction {
747 AlgoDir::Out => &[Direction::Out],
748 AlgoDir::In => &[Direction::In],
749 AlgoDir::Both => &[Direction::Out, Direction::In],
750 };
751 for &dir in dirs {
752 if !weighted {
753 degrees[i] += topo.neighbors(et, dir, id).len() as u64;
754 continue;
755 }
756 for &nbr in topo.neighbors(et, dir, id).as_ref() {
757 let (src, dst) = match dir {
758 Direction::Out => (id, nbr),
759 Direction::In => (nbr, id),
760 };
761 if edge_weight(
762 edge_props,
763 et,
764 src,
765 dst,
766 config.weight_prop.as_deref(),
767 config.min_weight,
768 )
769 .is_some()
770 {
771 degrees[i] += 1;
772 }
773 }
774 }
775 }
776 }
777
778 let mut scores: Vec<(String, u64)> = node_keys.into_iter().zip(degrees).collect();
779 scores.sort_by(|(ka, da), (kb, db)| db.cmp(da).then(ka.cmp(kb)));
780
781 DegreeReport { scores, truncated }
782}
783
784type WeightedAdj = Vec<Vec<(usize, f64)>>;
793
794struct AggregatedLevel {
796 renumbered: Vec<usize>,
800 n: usize,
801 adj: WeightedAdj,
802 self_weight: Vec<f64>,
803}
804
805#[derive(Debug, Clone, Serialize, Deserialize)]
807#[serde(default)]
808pub struct LouvainConfig {
809 pub edge_types: Vec<String>,
812 pub weight_prop: Option<String>,
815 pub min_weight: Option<f64>,
818 pub resolution: f64,
821 pub max_passes: u32,
823 pub max_sweeps: u32,
825 pub budget_ms: u64,
828 pub node_label: Option<String>,
831}
832
833impl Default for LouvainConfig {
834 fn default() -> Self {
835 Self {
836 edge_types: Vec::new(),
837 weight_prop: None,
838 min_weight: None,
839 resolution: 1.0,
840 max_passes: 10,
841 max_sweeps: 20,
842 budget_ms: 5_000,
843 node_label: None,
844 }
845 }
846}
847
848#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
850pub struct Community {
851 pub id: u32,
855 pub members: Vec<String>,
857 pub internal_weight: f64,
862 pub cohesion: f64,
866}
867
868#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
870pub struct CommunityReport {
871 pub communities: Vec<Community>,
873 pub modularity: f64,
875 pub truncated: bool,
877}
878
879fn local_moving(
888 n: usize,
889 adj: &WeightedAdj,
890 self_weight: &[f64],
891 resolution: f64,
892 max_sweeps: u32,
893 deadline: Option<Instant>,
894) -> (Vec<usize>, bool) {
895 let k: Vec<f64> = (0..n)
896 .map(|i| adj[i].iter().map(|&(_, w)| w).sum::<f64>() + 2.0 * self_weight[i])
897 .collect();
898 let m: f64 = k.iter().sum::<f64>() / 2.0;
899 let mut community_of: Vec<usize> = (0..n).collect();
900 if m <= 0.0 {
901 return (community_of, false);
902 }
903 let mut tot: Vec<f64> = k.clone();
904
905 for _sweep in 0..max_sweeps {
906 if let Some(dl) = deadline {
907 if Instant::now() >= dl {
908 return (community_of, true);
909 }
910 }
911 let mut improved = false;
912 for i in 0..n {
913 let ci = community_of[i];
914 tot[ci] -= k[i];
915
916 let mut neighbor_weights: BTreeMap<usize, f64> = BTreeMap::new();
920 for &(j, w) in &adj[i] {
921 if j == i {
922 continue; }
924 *neighbor_weights.entry(community_of[j]).or_insert(0.0) += w;
925 }
926
927 let gain = |c: usize, w_in: f64| -> f64 {
928 w_in / m - resolution * tot[c] * k[i] / (2.0 * m * m)
929 };
930
931 let mut best_c = ci;
932 let mut best_gain = gain(ci, neighbor_weights.get(&ci).copied().unwrap_or(0.0));
933 for (&c, &w_in) in &neighbor_weights {
934 if c == ci {
935 continue;
936 }
937 let g = gain(c, w_in);
938 if g > best_gain + 1e-12 {
939 best_gain = g;
940 best_c = c;
941 }
942 }
943
944 tot[best_c] += k[i];
945 if best_c != ci {
946 community_of[i] = best_c;
947 improved = true;
948 }
949 }
950 if !improved {
951 break;
952 }
953 }
954
955 (community_of, false)
956}
957
958fn aggregate(
967 n: usize,
968 adj: &WeightedAdj,
969 self_weight: &[f64],
970 community_of: &[usize],
971) -> Option<AggregatedLevel> {
972 let mut remap: BTreeMap<usize, usize> = BTreeMap::new();
973 let mut next_id = 0usize;
974 let mut renumbered: Vec<usize> = vec![0; n];
975 for (i, item) in renumbered.iter_mut().enumerate() {
976 let c = community_of[i];
977 let idx = *remap.entry(c).or_insert_with(|| {
978 let id = next_id;
979 next_id += 1;
980 id
981 });
982 *item = idx;
983 }
984 let new_n = next_id;
985 if new_n == n {
986 return None; }
988
989 let mut new_self_weight = vec![0.0; new_n];
990 let mut new_adj_map: Vec<BTreeMap<usize, f64>> = vec![BTreeMap::new(); new_n];
991 for i in 0..n {
992 let ci = renumbered[i];
993 new_self_weight[ci] += self_weight[i];
994 for &(j, w) in &adj[i] {
995 if j < i {
996 continue; }
998 let cj = renumbered[j];
999 if ci == cj {
1000 new_self_weight[ci] += w;
1001 } else {
1002 *new_adj_map[ci].entry(cj).or_insert(0.0) += w;
1003 *new_adj_map[cj].entry(ci).or_insert(0.0) += w;
1004 }
1005 }
1006 }
1007
1008 let new_adj: WeightedAdj = new_adj_map
1009 .into_iter()
1010 .map(|map| map.into_iter().collect())
1011 .collect();
1012
1013 Some(AggregatedLevel {
1014 renumbered,
1015 n: new_n,
1016 adj: new_adj,
1017 self_weight: new_self_weight,
1018 })
1019}
1020
1021pub(crate) fn louvain(
1029 topo: &TopologyView,
1030 idmap: &IdMap,
1031 syms: &Interner,
1032 labels: &[u32],
1033 edge_props: &EdgePropsView,
1034 config: &LouvainConfig,
1035) -> CommunityReport {
1036 let deadline = if config.budget_ms > 0 {
1037 Some(Instant::now() + Duration::from_millis(config.budget_ms))
1038 } else {
1039 None
1040 };
1041
1042 let (raw_ids, raw_keys) =
1047 live_nodes_for_label(idmap, syms, labels, config.node_label.as_deref());
1048 let mut order: Vec<usize> = (0..raw_ids.len()).collect();
1049 order.sort_by(|&a, &b| raw_keys[a].cmp(&raw_keys[b]));
1050 let node_ids: Vec<u32> = order.iter().map(|&i| raw_ids[i]).collect();
1051 let node_keys: Vec<String> = order.iter().map(|&i| raw_keys[i].clone()).collect();
1052 let n0 = node_ids.len();
1053
1054 if n0 == 0 {
1055 return CommunityReport {
1056 communities: Vec::new(),
1057 modularity: 0.0,
1058 truncated: false,
1059 };
1060 }
1061
1062 let mut id_to_idx: BTreeMap<u32, usize> = BTreeMap::new();
1063 for (i, &id) in node_ids.iter().enumerate() {
1064 id_to_idx.insert(id, i);
1065 }
1066
1067 let etypes = resolve_etypes_multi(syms, topo, &config.edge_types);
1068
1069 let mut edge_weight_map: BTreeMap<(usize, usize), f64> = BTreeMap::new();
1075 for &et in &etypes {
1076 for (i, &id) in node_ids.iter().enumerate() {
1077 for &nbr in topo.neighbors(et, Direction::Out, id).as_ref() {
1078 if nbr == id {
1079 continue; }
1081 let Some(&j) = id_to_idx.get(&nbr) else {
1082 continue; };
1084 let Some(w) = edge_weight(
1085 edge_props,
1086 et,
1087 id,
1088 nbr,
1089 config.weight_prop.as_deref(),
1090 config.min_weight,
1091 ) else {
1092 continue; };
1094 let key = if i < j { (i, j) } else { (j, i) };
1095 *edge_weight_map.entry(key).or_insert(0.0) += w;
1096 }
1097 }
1098 }
1099
1100 let m: f64 = edge_weight_map.values().sum();
1101
1102 let mut adj: WeightedAdj = vec![Vec::new(); n0];
1103 for (&(a, b), &w) in &edge_weight_map {
1104 adj[a].push((b, w));
1105 adj[b].push((a, w));
1106 }
1107 let mut self_weight: Vec<f64> = vec![0.0; n0];
1108
1109 let mut owner: Vec<usize> = (0..n0).collect();
1111 let mut truncated = false;
1112 let mut n = n0;
1113
1114 if m > 0.0 {
1115 'passes: for _pass in 0..config.max_passes {
1116 let (community_of, hit_budget) = local_moving(
1117 n,
1118 &adj,
1119 &self_weight,
1120 config.resolution,
1121 config.max_sweeps,
1122 deadline,
1123 );
1124 if hit_budget {
1125 owner = owner.iter().map(|&o| community_of[o]).collect();
1129 truncated = true;
1130 break 'passes;
1131 }
1132 let Some(level) = aggregate(n, &adj, &self_weight, &community_of) else {
1133 owner = owner.iter().map(|&o| community_of[o]).collect();
1136 break 'passes;
1137 };
1138 owner = owner.iter().map(|&o| level.renumbered[o]).collect();
1141 n = level.n;
1142 adj = level.adj;
1143 self_weight = level.self_weight;
1144 }
1145 }
1146
1147 let mut internal: BTreeMap<usize, f64> = BTreeMap::new();
1150 let mut leaving: BTreeMap<usize, f64> = BTreeMap::new();
1151 for (&(a, b), &w) in &edge_weight_map {
1152 let ca = owner[a];
1153 let cb = owner[b];
1154 if ca == cb {
1155 *internal.entry(ca).or_insert(0.0) += w;
1156 } else {
1157 *leaving.entry(ca).or_insert(0.0) += w;
1158 *leaving.entry(cb).or_insert(0.0) += w;
1159 }
1160 }
1161
1162 let mut members_by_community: BTreeMap<usize, Vec<String>> = BTreeMap::new();
1163 for (i, key) in node_keys.iter().enumerate() {
1164 members_by_community
1165 .entry(owner[i])
1166 .or_default()
1167 .push(key.clone());
1168 }
1169
1170 let modularity = if m > 0.0 {
1171 members_by_community
1172 .keys()
1173 .map(|c| {
1174 let internal_w = internal.get(c).copied().unwrap_or(0.0);
1175 let leaving_w = leaving.get(c).copied().unwrap_or(0.0);
1176 let sigma_tot = 2.0 * internal_w + leaving_w;
1177 internal_w / m - config.resolution * (sigma_tot * sigma_tot) / (4.0 * m * m)
1178 })
1179 .sum()
1180 } else {
1181 0.0
1182 };
1183
1184 let mut communities: Vec<Community> = members_by_community
1185 .into_iter()
1186 .map(|(c, mut members)| {
1187 members.sort();
1188 let internal_w = internal.get(&c).copied().unwrap_or(0.0);
1189 let leaving_w = leaving.get(&c).copied().unwrap_or(0.0);
1190 let cohesion = if internal_w + leaving_w > 0.0 {
1191 internal_w / (internal_w + leaving_w)
1192 } else {
1193 1.0
1194 };
1195 Community {
1196 id: 0, members,
1198 internal_weight: internal_w,
1199 cohesion,
1200 }
1201 })
1202 .collect();
1203
1204 communities.sort_by(|a, b| {
1205 b.members
1206 .len()
1207 .cmp(&a.members.len())
1208 .then_with(|| a.members[0].cmp(&b.members[0]))
1209 });
1210 for (i, c) in communities.iter_mut().enumerate() {
1211 c.id = i as u32;
1212 }
1213
1214 CommunityReport {
1215 communities,
1216 modularity,
1217 truncated,
1218 }
1219}