agentstatedeveloper_core/
community.rs1use std::collections::HashMap;
22
23pub fn detect_communities(nodes: &[String], edges: &[(String, String)]) -> HashMap<String, usize> {
28 let n = nodes.len();
29 let idx: HashMap<&str, usize> = nodes
30 .iter()
31 .enumerate()
32 .map(|(i, s)| (s.as_str(), i))
33 .collect();
34
35 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
37 {
38 let mut seen: std::collections::HashSet<(usize, usize)> = std::collections::HashSet::new();
39 for (a, b) in edges {
40 let (Some(&ia), Some(&ib)) = (idx.get(a.as_str()), idx.get(b.as_str())) else {
41 continue;
42 };
43 if ia == ib {
44 continue;
45 }
46 let key = if ia < ib { (ia, ib) } else { (ib, ia) };
47 if seen.insert(key) {
48 adj[ia].push(ib);
49 adj[ib].push(ia);
50 }
51 }
52 }
53
54 let deg: Vec<f64> = adj.iter().map(|a| a.len() as f64).collect();
55 let two_m: f64 = deg.iter().sum();
56 if two_m == 0.0 {
57 return nodes
59 .iter()
60 .enumerate()
61 .map(|(i, s)| (s.clone(), i))
62 .collect();
63 }
64
65 let mut comm: Vec<usize> = (0..n).collect();
66 let mut sigma_tot: Vec<f64> = deg.clone(); let mut improved = true;
69 let mut guard = 0;
70 while improved && guard < 50 {
71 improved = false;
72 guard += 1;
73 for i in 0..n {
74 if adj[i].is_empty() {
75 continue;
76 }
77 let ci = comm[i];
78 let ki = deg[i];
79 sigma_tot[ci] -= ki;
81 comm[i] = usize::MAX;
82
83 let mut k_in: HashMap<usize, f64> = HashMap::new();
85 for &j in &adj[i] {
86 if comm[j] != usize::MAX {
87 *k_in.entry(comm[j]).or_default() += 1.0;
88 }
89 }
90
91 let gain = |c: usize| k_in.get(&c).copied().unwrap_or(0.0) - sigma_tot[c] * ki / two_m;
94 let mut best_c = ci;
95 let mut best_gain = gain(ci);
96 for &c in k_in.keys() {
97 let g = gain(c);
98 if g > best_gain + 1e-12 || ((g - best_gain).abs() <= 1e-12 && c < best_c) {
99 best_gain = g;
100 best_c = c;
101 }
102 }
103
104 comm[i] = best_c;
105 sigma_tot[best_c] += ki;
106 if best_c != ci {
107 improved = true;
108 }
109 }
110 }
111
112 let mut canon: HashMap<usize, usize> = HashMap::new();
114 let mut next = 0usize;
115 let mut out = HashMap::with_capacity(n);
116 for (i, s) in nodes.iter().enumerate() {
117 let id = *canon.entry(comm[i]).or_insert_with(|| {
118 let v = next;
119 next += 1;
120 v
121 });
122 out.insert(s.clone(), id);
123 }
124 out
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 fn nodes(ns: &[&str]) -> Vec<String> {
132 ns.iter().map(|s| s.to_string()).collect()
133 }
134 fn edges(es: &[(&str, &str)]) -> Vec<(String, String)> {
135 es.iter()
136 .map(|(a, b)| (a.to_string(), b.to_string()))
137 .collect()
138 }
139 fn num_communities(m: &HashMap<String, usize>) -> usize {
140 m.values().collect::<std::collections::HashSet<_>>().len()
141 }
142
143 #[test]
144 fn two_triangles_one_bridge_stay_separate() {
145 let ns = nodes(&["a", "b", "c", "d", "e", "f"]);
148 let es = edges(&[
149 ("a", "b"),
150 ("b", "c"),
151 ("a", "c"), ("d", "e"),
153 ("e", "f"),
154 ("d", "f"), ("c", "d"), ]);
157 let comm = detect_communities(&ns, &es);
158 assert_eq!(num_communities(&comm), 2, "{comm:?}");
159 assert_eq!(comm["a"], comm["b"]);
160 assert_eq!(comm["b"], comm["c"]);
161 assert_eq!(comm["d"], comm["e"]);
162 assert_eq!(comm["e"], comm["f"]);
163 assert_ne!(comm["c"], comm["d"]);
164 }
165
166 #[test]
167 fn single_clique_is_one_community() {
168 let ns = nodes(&["a", "b", "c", "d"]);
169 let es = edges(&[
170 ("a", "b"),
171 ("a", "c"),
172 ("a", "d"),
173 ("b", "c"),
174 ("b", "d"),
175 ("c", "d"),
176 ]);
177 assert_eq!(num_communities(&detect_communities(&ns, &es)), 1);
178 }
179
180 #[test]
181 fn isolated_nodes_each_their_own() {
182 let ns = nodes(&["a", "b", "c"]);
183 let comm = detect_communities(&ns, &[]);
184 assert_eq!(num_communities(&comm), 3);
185 }
186
187 #[test]
188 fn deterministic_across_runs() {
189 let ns = nodes(&["a", "b", "c", "d", "e", "f"]);
190 let es = edges(&[
191 ("a", "b"),
192 ("b", "c"),
193 ("a", "c"),
194 ("d", "e"),
195 ("e", "f"),
196 ("d", "f"),
197 ("c", "d"),
198 ]);
199 assert_eq!(detect_communities(&ns, &es), detect_communities(&ns, &es));
200 }
201
202 #[test]
203 fn ignores_self_loops_and_unknown_nodes() {
204 let ns = nodes(&["a", "b"]);
205 let es = edges(&[("a", "a"), ("a", "b"), ("a", "ghost")]);
206 let comm = detect_communities(&ns, &es);
207 assert_eq!(comm["a"], comm["b"]); assert_eq!(num_communities(&comm), 1);
209 }
210}