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, deny_unknown_fields)]
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
232impl PageRankConfig {
233 pub fn validate(&self) -> Result<(), String> {
239 if !(0.0..=1.0).contains(&self.damping) {
240 return Err(format!(
241 "damping must be a number from 0 to 1, got {}",
242 self.damping
243 ));
244 }
245 Ok(())
246 }
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct PageRankReport {
252 pub scores: Vec<(String, f64)>,
255 pub converged: bool,
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
263#[serde(rename_all = "lowercase")]
264pub enum AlgoDir {
265 Out,
267 In,
269 Both,
272}
273
274impl From<Dir> for AlgoDir {
275 fn from(d: Dir) -> Self {
276 match d {
277 Dir::Out => AlgoDir::Out,
278 Dir::In => AlgoDir::In,
279 Dir::Both => AlgoDir::Both,
280 }
281 }
282}
283
284pub(crate) fn pagerank(
288 topo: &TopologyView,
289 idmap: &IdMap,
290 syms: &Interner,
291 labels: &[u32],
292 edge_props: &EdgePropsView,
293 config: &PageRankConfig,
294) -> PageRankReport {
295 let weighted = config.weight_prop.is_some() || config.min_weight.is_some();
296 let deadline = if config.budget_ms > 0 {
297 Some(Instant::now() + Duration::from_millis(config.budget_ms))
298 } else {
299 None
300 };
301
302 let (node_ids, node_keys) = live_nodes(idmap, labels);
303 let n = node_ids.len();
304
305 if n == 0 {
306 return PageRankReport {
307 scores: Vec::new(),
308 converged: true,
309 };
310 }
311
312 let max_id = topo.etypes().count(); let _ = max_id;
315 let mut id_to_idx: BTreeMap<u32, usize> = BTreeMap::new();
316 for (i, &id) in node_ids.iter().enumerate() {
317 id_to_idx.insert(id, i);
318 }
319
320 let etype_filter = match resolve_etype(syms, config.edge_type.as_deref()) {
322 None => {
323 let score = 1.0 / n as f64;
325 let mut scores: Vec<(String, f64)> =
326 node_keys.iter().map(|k| (k.clone(), score)).collect();
327 scores.sort_by(|(ka, sa), (kb, sb)| {
328 sb.partial_cmp(sa)
329 .unwrap_or(std::cmp::Ordering::Equal)
330 .then(ka.cmp(kb))
331 });
332 return PageRankReport {
333 scores,
334 converged: true,
335 };
336 }
337 Some(f) => f,
338 };
339
340 let etypes = etypes_filtered(topo, etype_filter);
341
342 let mut send_to: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
349
350 for &et in &etypes {
351 for (i, &id) in node_ids.iter().enumerate() {
352 let dirs: &[Direction] = match config.direction {
353 AlgoDir::Out => &[Direction::Out],
354 AlgoDir::In => &[Direction::In],
355 AlgoDir::Both => &[Direction::Out, Direction::In],
356 };
357 for &dir in dirs {
358 for &nbr in topo.neighbors(et, dir, id).as_ref() {
359 let Some(&j) = id_to_idx.get(&nbr) else {
360 continue;
361 };
362 if weighted {
363 let Some(w) = edge_weight(
364 edge_props,
365 et,
366 id,
367 nbr,
368 config.weight_prop.as_deref(),
369 config.min_weight,
370 ) else {
371 continue; };
373 if let Some(entry) = send_to[i].iter_mut().find(|(k, _)| *k == j) {
374 entry.1 += w;
375 } else {
376 send_to[i].push((j, w));
377 }
378 } else if !send_to[i].iter().any(|(k, _)| *k == j) {
379 send_to[i].push((j, 1.0));
380 }
381 }
382 }
383 }
384 }
385
386 let mut receive_from: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
390 let mut dangling: Vec<usize> = Vec::new();
391
392 for (i, send) in send_to.iter().enumerate() {
393 let out_weight: f64 = send.iter().map(|(_, w)| w).sum();
394 if send.is_empty() || out_weight <= 0.0 {
395 dangling.push(i);
396 } else {
397 for &(j, w) in send {
398 receive_from[j].push((i, w / out_weight));
399 }
400 }
401 }
402
403 let nf = n as f64;
405 let d = config.damping;
406 let teleport = (1.0 - d) / nf;
407 let mut pr: Vec<f64> = vec![1.0 / nf; n];
408 let mut converged = false;
409
410 for _iter in 0..config.max_iters {
411 if let Some(dl) = deadline {
413 if Instant::now() >= dl {
414 break;
415 }
416 }
417
418 let dangling_sum: f64 = dangling.iter().map(|&i| pr[i]).sum::<f64>() * d / nf;
420
421 let mut new_pr = vec![teleport + dangling_sum; n];
422 for j in 0..n {
423 let received: f64 = receive_from[j].iter().map(|&(i, w)| pr[i] * w).sum();
424 new_pr[j] += d * received;
425 }
426
427 let delta: f64 = pr
429 .iter()
430 .zip(new_pr.iter())
431 .map(|(a, b)| (a - b).abs())
432 .sum();
433 pr = new_pr;
434
435 if delta < config.tol {
436 converged = true;
437 break;
438 }
439 }
440
441 let mut scores: Vec<(String, f64)> = node_keys.into_iter().zip(pr).collect();
443 scores.sort_by(|(ka, sa), (kb, sb)| {
444 sb.partial_cmp(sa)
445 .unwrap_or(std::cmp::Ordering::Equal)
446 .then(ka.cmp(kb))
447 });
448
449 PageRankReport { scores, converged }
450}
451
452#[derive(Debug, Clone, Serialize, Deserialize)]
458#[serde(default, deny_unknown_fields)]
459pub struct WccConfig {
460 pub edge_type: Option<String>,
462 pub budget_ms: u64,
464 pub weight_prop: Option<String>,
468 pub min_weight: Option<f64>,
471}
472
473impl Default for WccConfig {
474 fn default() -> Self {
475 Self {
476 edge_type: None,
477 budget_ms: 5_000,
478 weight_prop: None,
479 min_weight: None,
480 }
481 }
482}
483
484#[derive(Debug, Clone, Serialize, Deserialize)]
486pub struct WccReport {
487 pub components: Vec<(String, String)>,
490 pub truncated: bool,
492}
493
494struct UnionFind {
496 parent: Vec<usize>,
497 rank: Vec<u8>,
498}
499
500impl UnionFind {
501 fn new(n: usize) -> Self {
502 Self {
503 parent: (0..n).collect(),
504 rank: vec![0; n],
505 }
506 }
507
508 fn find(&mut self, mut x: usize) -> usize {
509 while self.parent[x] != x {
510 self.parent[x] = self.parent[self.parent[x]]; x = self.parent[x];
512 }
513 x
514 }
515
516 fn union(&mut self, a: usize, b: usize) {
517 let ra = self.find(a);
518 let rb = self.find(b);
519 if ra == rb {
520 return;
521 }
522 match self.rank[ra].cmp(&self.rank[rb]) {
523 std::cmp::Ordering::Less => self.parent[ra] = rb,
524 std::cmp::Ordering::Greater => self.parent[rb] = ra,
525 std::cmp::Ordering::Equal => {
526 self.parent[rb] = ra;
527 self.rank[ra] += 1;
528 }
529 }
530 }
531}
532
533pub(crate) fn wcc(
537 topo: &TopologyView,
538 idmap: &IdMap,
539 syms: &Interner,
540 labels: &[u32],
541 edge_props: &EdgePropsView,
542 config: &WccConfig,
543) -> WccReport {
544 let weighted = config.weight_prop.is_some() || config.min_weight.is_some();
545 let deadline = if config.budget_ms > 0 {
546 Some(Instant::now() + Duration::from_millis(config.budget_ms))
547 } else {
548 None
549 };
550
551 let (node_ids, node_keys) = live_nodes(idmap, labels);
552 let n = node_ids.len();
553
554 if n == 0 {
555 return WccReport {
556 components: Vec::new(),
557 truncated: false,
558 };
559 }
560
561 let mut id_to_idx: BTreeMap<u32, usize> = BTreeMap::new();
563 for (i, &id) in node_ids.iter().enumerate() {
564 id_to_idx.insert(id, i);
565 }
566
567 let etype_filter = match resolve_etype(syms, config.edge_type.as_deref()) {
569 None => {
570 let mut components: Vec<(String, String)> =
572 node_keys.iter().map(|k| (k.clone(), k.clone())).collect();
573 components.sort();
574 return WccReport {
575 components,
576 truncated: false,
577 };
578 }
579 Some(f) => f,
580 };
581
582 let etypes = etypes_filtered(topo, etype_filter);
583
584 let mut uf = UnionFind::new(n);
585 let mut truncated = false;
586
587 'outer: for &et in &etypes {
589 for (i, &id) in node_ids.iter().enumerate() {
590 if let Some(dl) = deadline {
591 if Instant::now() >= dl {
592 truncated = true;
593 break 'outer;
594 }
595 }
596 for &nbr in topo.neighbors(et, Direction::Out, id).as_ref() {
598 if let Some(&j) = id_to_idx.get(&nbr) {
599 if weighted
600 && edge_weight(
601 edge_props,
602 et,
603 id,
604 nbr,
605 config.weight_prop.as_deref(),
606 config.min_weight,
607 )
608 .is_none()
609 {
610 continue; }
612 uf.union(i, j);
613 }
614 }
615 for &nbr in topo.neighbors(et, Direction::In, id).as_ref() {
619 if let Some(&j) = id_to_idx.get(&nbr) {
620 if weighted
621 && edge_weight(
622 edge_props,
623 et,
624 nbr,
625 id,
626 config.weight_prop.as_deref(),
627 config.min_weight,
628 )
629 .is_none()
630 {
631 continue; }
633 uf.union(i, j);
634 }
635 }
636 }
637 }
638
639 let mut root_min_key: BTreeMap<usize, &str> = BTreeMap::new();
641 for (i, key_str) in node_keys.iter().enumerate() {
642 let root = uf.find(i);
643 let key = key_str.as_str();
644 let entry = root_min_key.entry(root).or_insert(key);
645 if key < *entry {
646 *entry = key;
647 }
648 }
649
650 let mut components: Vec<(String, String)> = node_keys
651 .iter()
652 .enumerate()
653 .map(|(i, key_str)| {
654 let root = uf.find(i);
655 let comp_id = root_min_key[&root].to_string();
656 (key_str.clone(), comp_id)
657 })
658 .collect();
659 components.sort_by(|(ka, ca), (kb, cb)| ca.cmp(cb).then(ka.cmp(kb)));
660
661 WccReport {
662 components,
663 truncated,
664 }
665}
666
667#[derive(Debug, Clone, Serialize, Deserialize)]
673#[serde(default, deny_unknown_fields)]
674pub struct DegreeConfig {
675 pub edge_type: Option<String>,
677 pub direction: AlgoDir,
679 pub budget_ms: u64,
681 pub weight_prop: Option<String>,
686 pub min_weight: Option<f64>,
689}
690
691impl Default for DegreeConfig {
692 fn default() -> Self {
693 Self {
694 edge_type: None,
695 direction: AlgoDir::Both,
696 budget_ms: 5_000,
697 weight_prop: None,
698 min_weight: None,
699 }
700 }
701}
702
703#[derive(Debug, Clone, Serialize, Deserialize)]
705pub struct DegreeReport {
706 pub scores: Vec<(String, u64)>,
708 pub truncated: bool,
710}
711
712pub(crate) fn degree_centrality(
714 topo: &TopologyView,
715 idmap: &IdMap,
716 syms: &Interner,
717 labels: &[u32],
718 edge_props: &EdgePropsView,
719 config: &DegreeConfig,
720) -> DegreeReport {
721 let weighted = config.weight_prop.is_some() || config.min_weight.is_some();
722 let deadline = if config.budget_ms > 0 {
723 Some(Instant::now() + Duration::from_millis(config.budget_ms))
724 } else {
725 None
726 };
727
728 let (node_ids, node_keys) = live_nodes(idmap, labels);
729 let n = node_ids.len();
730
731 if n == 0 {
732 return DegreeReport {
733 scores: Vec::new(),
734 truncated: false,
735 };
736 }
737
738 let etype_filter = match resolve_etype(syms, config.edge_type.as_deref()) {
740 None => {
741 let scores = node_keys.iter().map(|k| (k.clone(), 0u64)).collect();
743 return DegreeReport {
744 scores,
745 truncated: false,
746 };
747 }
748 Some(f) => f,
749 };
750
751 let etypes = etypes_filtered(topo, etype_filter);
752 let mut degrees: Vec<u64> = vec![0u64; n];
753 let mut truncated = false;
754
755 for (i, &id) in node_ids.iter().enumerate() {
756 if let Some(dl) = deadline {
757 if Instant::now() >= dl {
758 truncated = true;
759 break;
760 }
761 }
762 for &et in &etypes {
763 let dirs: &[Direction] = match config.direction {
764 AlgoDir::Out => &[Direction::Out],
765 AlgoDir::In => &[Direction::In],
766 AlgoDir::Both => &[Direction::Out, Direction::In],
767 };
768 for &dir in dirs {
769 if !weighted {
770 degrees[i] += topo.neighbors(et, dir, id).len() as u64;
771 continue;
772 }
773 for &nbr in topo.neighbors(et, dir, id).as_ref() {
774 let (src, dst) = match dir {
775 Direction::Out => (id, nbr),
776 Direction::In => (nbr, id),
777 };
778 if edge_weight(
779 edge_props,
780 et,
781 src,
782 dst,
783 config.weight_prop.as_deref(),
784 config.min_weight,
785 )
786 .is_some()
787 {
788 degrees[i] += 1;
789 }
790 }
791 }
792 }
793 }
794
795 let mut scores: Vec<(String, u64)> = node_keys.into_iter().zip(degrees).collect();
796 scores.sort_by(|(ka, da), (kb, db)| db.cmp(da).then(ka.cmp(kb)));
797
798 DegreeReport { scores, truncated }
799}
800
801type WeightedAdj = Vec<Vec<(usize, f64)>>;
810
811struct AggregatedLevel {
813 renumbered: Vec<usize>,
817 n: usize,
818 adj: WeightedAdj,
819 self_weight: Vec<f64>,
820}
821
822#[derive(Debug, Clone, Serialize, Deserialize)]
824#[serde(default)]
825pub struct LouvainConfig {
826 pub edge_types: Vec<String>,
829 pub weight_prop: Option<String>,
832 pub min_weight: Option<f64>,
835 pub resolution: f64,
838 pub max_passes: u32,
840 pub max_sweeps: u32,
842 pub budget_ms: u64,
845 pub node_label: Option<String>,
848}
849
850impl Default for LouvainConfig {
851 fn default() -> Self {
852 Self {
853 edge_types: Vec::new(),
854 weight_prop: None,
855 min_weight: None,
856 resolution: 1.0,
857 max_passes: 10,
858 max_sweeps: 20,
859 budget_ms: 5_000,
860 node_label: None,
861 }
862 }
863}
864
865#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
867pub struct Community {
868 pub id: u32,
872 pub members: Vec<String>,
874 pub internal_weight: f64,
879 pub cohesion: f64,
883}
884
885#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
887pub struct CommunityReport {
888 pub communities: Vec<Community>,
890 pub modularity: f64,
892 pub truncated: bool,
894}
895
896fn local_moving(
905 n: usize,
906 adj: &WeightedAdj,
907 self_weight: &[f64],
908 resolution: f64,
909 max_sweeps: u32,
910 deadline: Option<Instant>,
911) -> (Vec<usize>, bool) {
912 let k: Vec<f64> = (0..n)
913 .map(|i| adj[i].iter().map(|&(_, w)| w).sum::<f64>() + 2.0 * self_weight[i])
914 .collect();
915 let m: f64 = k.iter().sum::<f64>() / 2.0;
916 let mut community_of: Vec<usize> = (0..n).collect();
917 if m <= 0.0 {
918 return (community_of, false);
919 }
920 let mut tot: Vec<f64> = k.clone();
921
922 for _sweep in 0..max_sweeps {
923 if let Some(dl) = deadline {
924 if Instant::now() >= dl {
925 return (community_of, true);
926 }
927 }
928 let mut improved = false;
929 for i in 0..n {
930 let ci = community_of[i];
931 tot[ci] -= k[i];
932
933 let mut neighbor_weights: BTreeMap<usize, f64> = BTreeMap::new();
937 for &(j, w) in &adj[i] {
938 if j == i {
939 continue; }
941 *neighbor_weights.entry(community_of[j]).or_insert(0.0) += w;
942 }
943
944 let gain = |c: usize, w_in: f64| -> f64 {
945 w_in / m - resolution * tot[c] * k[i] / (2.0 * m * m)
946 };
947
948 let mut best_c = ci;
949 let mut best_gain = gain(ci, neighbor_weights.get(&ci).copied().unwrap_or(0.0));
950 for (&c, &w_in) in &neighbor_weights {
951 if c == ci {
952 continue;
953 }
954 let g = gain(c, w_in);
955 if g > best_gain + 1e-12 {
956 best_gain = g;
957 best_c = c;
958 }
959 }
960
961 tot[best_c] += k[i];
962 if best_c != ci {
963 community_of[i] = best_c;
964 improved = true;
965 }
966 }
967 if !improved {
968 break;
969 }
970 }
971
972 (community_of, false)
973}
974
975fn aggregate(
984 n: usize,
985 adj: &WeightedAdj,
986 self_weight: &[f64],
987 community_of: &[usize],
988) -> Option<AggregatedLevel> {
989 let mut remap: BTreeMap<usize, usize> = BTreeMap::new();
990 let mut next_id = 0usize;
991 let mut renumbered: Vec<usize> = vec![0; n];
992 for (i, item) in renumbered.iter_mut().enumerate() {
993 let c = community_of[i];
994 let idx = *remap.entry(c).or_insert_with(|| {
995 let id = next_id;
996 next_id += 1;
997 id
998 });
999 *item = idx;
1000 }
1001 let new_n = next_id;
1002 if new_n == n {
1003 return None; }
1005
1006 let mut new_self_weight = vec![0.0; new_n];
1007 let mut new_adj_map: Vec<BTreeMap<usize, f64>> = vec![BTreeMap::new(); new_n];
1008 for i in 0..n {
1009 let ci = renumbered[i];
1010 new_self_weight[ci] += self_weight[i];
1011 for &(j, w) in &adj[i] {
1012 if j < i {
1013 continue; }
1015 let cj = renumbered[j];
1016 if ci == cj {
1017 new_self_weight[ci] += w;
1018 } else {
1019 *new_adj_map[ci].entry(cj).or_insert(0.0) += w;
1020 *new_adj_map[cj].entry(ci).or_insert(0.0) += w;
1021 }
1022 }
1023 }
1024
1025 let new_adj: WeightedAdj = new_adj_map
1026 .into_iter()
1027 .map(|map| map.into_iter().collect())
1028 .collect();
1029
1030 Some(AggregatedLevel {
1031 renumbered,
1032 n: new_n,
1033 adj: new_adj,
1034 self_weight: new_self_weight,
1035 })
1036}
1037
1038pub(crate) fn louvain(
1046 topo: &TopologyView,
1047 idmap: &IdMap,
1048 syms: &Interner,
1049 labels: &[u32],
1050 edge_props: &EdgePropsView,
1051 config: &LouvainConfig,
1052) -> CommunityReport {
1053 let deadline = if config.budget_ms > 0 {
1054 Some(Instant::now() + Duration::from_millis(config.budget_ms))
1055 } else {
1056 None
1057 };
1058
1059 let (raw_ids, raw_keys) =
1064 live_nodes_for_label(idmap, syms, labels, config.node_label.as_deref());
1065 let mut order: Vec<usize> = (0..raw_ids.len()).collect();
1066 order.sort_by(|&a, &b| raw_keys[a].cmp(&raw_keys[b]));
1067 let node_ids: Vec<u32> = order.iter().map(|&i| raw_ids[i]).collect();
1068 let node_keys: Vec<String> = order.iter().map(|&i| raw_keys[i].clone()).collect();
1069 let n0 = node_ids.len();
1070
1071 if n0 == 0 {
1072 return CommunityReport {
1073 communities: Vec::new(),
1074 modularity: 0.0,
1075 truncated: false,
1076 };
1077 }
1078
1079 let mut id_to_idx: BTreeMap<u32, usize> = BTreeMap::new();
1080 for (i, &id) in node_ids.iter().enumerate() {
1081 id_to_idx.insert(id, i);
1082 }
1083
1084 let etypes = resolve_etypes_multi(syms, topo, &config.edge_types);
1085
1086 let mut edge_weight_map: BTreeMap<(usize, usize), f64> = BTreeMap::new();
1092 for &et in &etypes {
1093 for (i, &id) in node_ids.iter().enumerate() {
1094 for &nbr in topo.neighbors(et, Direction::Out, id).as_ref() {
1095 if nbr == id {
1096 continue; }
1098 let Some(&j) = id_to_idx.get(&nbr) else {
1099 continue; };
1101 let Some(w) = edge_weight(
1102 edge_props,
1103 et,
1104 id,
1105 nbr,
1106 config.weight_prop.as_deref(),
1107 config.min_weight,
1108 ) else {
1109 continue; };
1111 let key = if i < j { (i, j) } else { (j, i) };
1112 *edge_weight_map.entry(key).or_insert(0.0) += w;
1113 }
1114 }
1115 }
1116
1117 let m: f64 = edge_weight_map.values().sum();
1118
1119 let mut adj: WeightedAdj = vec![Vec::new(); n0];
1120 for (&(a, b), &w) in &edge_weight_map {
1121 adj[a].push((b, w));
1122 adj[b].push((a, w));
1123 }
1124 let mut self_weight: Vec<f64> = vec![0.0; n0];
1125
1126 let mut owner: Vec<usize> = (0..n0).collect();
1128 let mut truncated = false;
1129 let mut n = n0;
1130
1131 if m > 0.0 {
1132 'passes: for _pass in 0..config.max_passes {
1133 let (community_of, hit_budget) = local_moving(
1134 n,
1135 &adj,
1136 &self_weight,
1137 config.resolution,
1138 config.max_sweeps,
1139 deadline,
1140 );
1141 if hit_budget {
1142 owner = owner.iter().map(|&o| community_of[o]).collect();
1146 truncated = true;
1147 break 'passes;
1148 }
1149 let Some(level) = aggregate(n, &adj, &self_weight, &community_of) else {
1150 owner = owner.iter().map(|&o| community_of[o]).collect();
1153 break 'passes;
1154 };
1155 owner = owner.iter().map(|&o| level.renumbered[o]).collect();
1158 n = level.n;
1159 adj = level.adj;
1160 self_weight = level.self_weight;
1161 }
1162 }
1163
1164 let mut internal: BTreeMap<usize, f64> = BTreeMap::new();
1167 let mut leaving: BTreeMap<usize, f64> = BTreeMap::new();
1168 for (&(a, b), &w) in &edge_weight_map {
1169 let ca = owner[a];
1170 let cb = owner[b];
1171 if ca == cb {
1172 *internal.entry(ca).or_insert(0.0) += w;
1173 } else {
1174 *leaving.entry(ca).or_insert(0.0) += w;
1175 *leaving.entry(cb).or_insert(0.0) += w;
1176 }
1177 }
1178
1179 let mut members_by_community: BTreeMap<usize, Vec<String>> = BTreeMap::new();
1180 for (i, key) in node_keys.iter().enumerate() {
1181 members_by_community
1182 .entry(owner[i])
1183 .or_default()
1184 .push(key.clone());
1185 }
1186
1187 let modularity = if m > 0.0 {
1188 members_by_community
1189 .keys()
1190 .map(|c| {
1191 let internal_w = internal.get(c).copied().unwrap_or(0.0);
1192 let leaving_w = leaving.get(c).copied().unwrap_or(0.0);
1193 let sigma_tot = 2.0 * internal_w + leaving_w;
1194 internal_w / m - config.resolution * (sigma_tot * sigma_tot) / (4.0 * m * m)
1195 })
1196 .sum()
1197 } else {
1198 0.0
1199 };
1200
1201 let mut communities: Vec<Community> = members_by_community
1202 .into_iter()
1203 .map(|(c, mut members)| {
1204 members.sort();
1205 let internal_w = internal.get(&c).copied().unwrap_or(0.0);
1206 let leaving_w = leaving.get(&c).copied().unwrap_or(0.0);
1207 let cohesion = if internal_w + leaving_w > 0.0 {
1208 internal_w / (internal_w + leaving_w)
1209 } else {
1210 1.0
1211 };
1212 Community {
1213 id: 0, members,
1215 internal_weight: internal_w,
1216 cohesion,
1217 }
1218 })
1219 .collect();
1220
1221 communities.sort_by(|a, b| {
1222 b.members
1223 .len()
1224 .cmp(&a.members.len())
1225 .then_with(|| a.members[0].cmp(&b.members[0]))
1226 });
1227 for (i, c) in communities.iter_mut().enumerate() {
1228 c.id = i as u32;
1229 }
1230
1231 CommunityReport {
1232 communities,
1233 modularity,
1234 truncated,
1235 }
1236}