graphos_adapters/plugins/algorithms/
community.rs1use std::sync::OnceLock;
7
8use graphos_common::types::{NodeId, Value};
9use graphos_common::utils::error::Result;
10use graphos_common::utils::hash::{FxHashMap, FxHashSet};
11use graphos_core::graph::Direction;
12use graphos_core::graph::lpg::LpgStore;
13
14use super::super::{AlgorithmResult, ParameterDef, ParameterType, Parameters};
15use super::traits::{ComponentResultBuilder, GraphAlgorithm};
16
17pub fn label_propagation(store: &LpgStore, max_iterations: usize) -> FxHashMap<NodeId, u64> {
40 let nodes = store.node_ids();
41 let n = nodes.len();
42
43 if n == 0 {
44 return FxHashMap::default();
45 }
46
47 let mut labels: FxHashMap<NodeId, u64> = FxHashMap::default();
49 for (idx, &node) in nodes.iter().enumerate() {
50 labels.insert(node, idx as u64);
51 }
52
53 let max_iter = if max_iterations == 0 {
54 n * 10
55 } else {
56 max_iterations
57 };
58
59 for _ in 0..max_iter {
60 let mut changed = false;
61
62 for &node in &nodes {
64 let mut label_counts: FxHashMap<u64, usize> = FxHashMap::default();
66
67 for (neighbor, _) in store.edges_from(node, Direction::Outgoing) {
69 if let Some(&label) = labels.get(&neighbor) {
70 *label_counts.entry(label).or_insert(0) += 1;
71 }
72 }
73
74 for &other_node in &nodes {
76 for (neighbor, _) in store.edges_from(other_node, Direction::Outgoing) {
77 if neighbor == node {
78 if let Some(&label) = labels.get(&other_node) {
79 *label_counts.entry(label).or_insert(0) += 1;
80 }
81 }
82 }
83 }
84
85 if label_counts.is_empty() {
86 continue;
87 }
88
89 let max_count = *label_counts.values().max().unwrap_or(&0);
91 let max_labels: Vec<u64> = label_counts
92 .into_iter()
93 .filter(|&(_, count)| count == max_count)
94 .map(|(label, _)| label)
95 .collect();
96
97 let new_label = *max_labels.iter().min().unwrap();
99 let current_label = *labels.get(&node).unwrap();
100
101 if new_label != current_label {
102 labels.insert(node, new_label);
103 changed = true;
104 }
105 }
106
107 if !changed {
108 break;
109 }
110 }
111
112 let unique_labels: FxHashSet<u64> = labels.values().copied().collect();
114 let mut label_map: FxHashMap<u64, u64> = FxHashMap::default();
115 for (idx, label) in unique_labels.into_iter().enumerate() {
116 label_map.insert(label, idx as u64);
117 }
118
119 labels
120 .into_iter()
121 .map(|(node, label)| (node, *label_map.get(&label).unwrap()))
122 .collect()
123}
124
125#[derive(Debug, Clone)]
131pub struct LouvainResult {
132 pub communities: FxHashMap<NodeId, u64>,
134 pub modularity: f64,
136 pub num_communities: usize,
138}
139
140pub fn louvain(store: &LpgStore, resolution: f64) -> LouvainResult {
160 let nodes = store.node_ids();
161 let n = nodes.len();
162
163 if n == 0 {
164 return LouvainResult {
165 communities: FxHashMap::default(),
166 modularity: 0.0,
167 num_communities: 0,
168 };
169 }
170
171 let mut node_to_idx: FxHashMap<NodeId, usize> = FxHashMap::default();
173 for (idx, &node) in nodes.iter().enumerate() {
174 node_to_idx.insert(node, idx);
175 }
176
177 let mut weights: Vec<FxHashMap<usize, f64>> = vec![FxHashMap::default(); n];
180 let mut total_weight = 0.0;
181
182 for &node in &nodes {
183 let i = *node_to_idx.get(&node).unwrap();
184 for (neighbor, _edge_id) in store.edges_from(node, Direction::Outgoing) {
185 if let Some(&j) = node_to_idx.get(&neighbor) {
186 let w = 1.0; *weights[i].entry(j).or_insert(0.0) += w;
189 *weights[j].entry(i).or_insert(0.0) += w;
190 total_weight += w;
191 }
192 }
193 }
194
195 if total_weight == 0.0 {
197 let communities: FxHashMap<NodeId, u64> = nodes
198 .iter()
199 .enumerate()
200 .map(|(idx, &node)| (node, idx as u64))
201 .collect();
202 return LouvainResult {
203 communities,
204 modularity: 0.0,
205 num_communities: n,
206 };
207 }
208
209 let degrees: Vec<f64> = (0..n).map(|i| weights[i].values().sum()).collect();
211
212 let mut community: Vec<usize> = (0..n).collect();
214
215 let mut community_internal: FxHashMap<usize, f64> = FxHashMap::default();
217 let mut community_total: FxHashMap<usize, f64> = FxHashMap::default();
218
219 for i in 0..n {
220 community_total.insert(i, degrees[i]);
221 community_internal.insert(i, weights[i].get(&i).copied().unwrap_or(0.0));
222 }
223
224 let mut improved = true;
226 while improved {
227 improved = false;
228
229 for i in 0..n {
230 let current_comm = community[i];
231
232 let mut comm_links: FxHashMap<usize, f64> = FxHashMap::default();
234 for (&j, &w) in &weights[i] {
235 let c = community[j];
236 *comm_links.entry(c).or_insert(0.0) += w;
237 }
238
239 let mut best_delta = 0.0;
241 let mut best_comm = current_comm;
242
243 let ki = degrees[i];
245 let ki_in = comm_links.get(¤t_comm).copied().unwrap_or(0.0);
246
247 for (&target_comm, &k_i_to_comm) in &comm_links {
248 if target_comm == current_comm {
249 continue;
250 }
251
252 let sigma_tot = *community_total.get(&target_comm).unwrap_or(&0.0);
253
254 let delta = resolution
256 * (k_i_to_comm
257 - ki_in
258 - ki * (sigma_tot - community_total.get(¤t_comm).unwrap_or(&0.0)
259 + ki)
260 / (2.0 * total_weight));
261
262 if delta > best_delta {
263 best_delta = delta;
264 best_comm = target_comm;
265 }
266 }
267
268 if best_comm != current_comm {
269 *community_total.entry(current_comm).or_insert(0.0) -= ki;
272 *community_internal.entry(current_comm).or_insert(0.0) -=
273 2.0 * ki_in + weights[i].get(&i).copied().unwrap_or(0.0);
274
275 community[i] = best_comm;
276
277 *community_total.entry(best_comm).or_insert(0.0) += ki;
278 let k_i_best = comm_links.get(&best_comm).copied().unwrap_or(0.0);
279 *community_internal.entry(best_comm).or_insert(0.0) +=
280 2.0 * k_i_best + weights[i].get(&i).copied().unwrap_or(0.0);
281
282 improved = true;
283 }
284 }
285 }
286
287 let unique_comms: FxHashSet<usize> = community.iter().copied().collect();
289 let mut comm_map: FxHashMap<usize, u64> = FxHashMap::default();
290 for (idx, c) in unique_comms.iter().enumerate() {
291 comm_map.insert(*c, idx as u64);
292 }
293
294 let communities: FxHashMap<NodeId, u64> = nodes
295 .iter()
296 .enumerate()
297 .map(|(i, &node)| (node, *comm_map.get(&community[i]).unwrap()))
298 .collect();
299
300 let modularity = compute_modularity(&weights, &community, total_weight, resolution);
302
303 LouvainResult {
304 communities,
305 modularity,
306 num_communities: unique_comms.len(),
307 }
308}
309
310fn compute_modularity(
312 weights: &[FxHashMap<usize, f64>],
313 community: &[usize],
314 total_weight: f64,
315 resolution: f64,
316) -> f64 {
317 let n = community.len();
318 let m2 = 2.0 * total_weight;
319
320 if m2 == 0.0 {
321 return 0.0;
322 }
323
324 let degrees: Vec<f64> = (0..n).map(|i| weights[i].values().sum()).collect();
325
326 let mut modularity = 0.0;
327
328 for i in 0..n {
329 for (&j, &a_ij) in &weights[i] {
330 if community[i] == community[j] {
331 modularity += a_ij - resolution * degrees[i] * degrees[j] / m2;
332 }
333 }
334 }
335
336 modularity / m2
337}
338
339pub fn community_count(communities: &FxHashMap<NodeId, u64>) -> usize {
341 let unique: FxHashSet<u64> = communities.values().copied().collect();
342 unique.len()
343}
344
345static LABEL_PROP_PARAMS: OnceLock<Vec<ParameterDef>> = OnceLock::new();
351
352fn label_prop_params() -> &'static [ParameterDef] {
353 LABEL_PROP_PARAMS.get_or_init(|| {
354 vec![ParameterDef {
355 name: "max_iterations".to_string(),
356 description: "Maximum iterations (0 for unlimited, default: 100)".to_string(),
357 param_type: ParameterType::Integer,
358 required: false,
359 default: Some("100".to_string()),
360 }]
361 })
362}
363
364pub struct LabelPropagationAlgorithm;
366
367impl GraphAlgorithm for LabelPropagationAlgorithm {
368 fn name(&self) -> &str {
369 "label_propagation"
370 }
371
372 fn description(&self) -> &str {
373 "Label Propagation community detection"
374 }
375
376 fn parameters(&self) -> &[ParameterDef] {
377 label_prop_params()
378 }
379
380 fn execute(&self, store: &LpgStore, params: &Parameters) -> Result<AlgorithmResult> {
381 let max_iter = params.get_int("max_iterations").unwrap_or(100) as usize;
382
383 let communities = label_propagation(store, max_iter);
384
385 let mut builder = ComponentResultBuilder::with_capacity(communities.len());
386 for (node, community_id) in communities {
387 builder.push(node, community_id);
388 }
389
390 Ok(builder.build())
391 }
392}
393
394static LOUVAIN_PARAMS: OnceLock<Vec<ParameterDef>> = OnceLock::new();
396
397fn louvain_params() -> &'static [ParameterDef] {
398 LOUVAIN_PARAMS.get_or_init(|| {
399 vec![ParameterDef {
400 name: "resolution".to_string(),
401 description: "Resolution parameter (default: 1.0)".to_string(),
402 param_type: ParameterType::Float,
403 required: false,
404 default: Some("1.0".to_string()),
405 }]
406 })
407}
408
409pub struct LouvainAlgorithm;
411
412impl GraphAlgorithm for LouvainAlgorithm {
413 fn name(&self) -> &str {
414 "louvain"
415 }
416
417 fn description(&self) -> &str {
418 "Louvain community detection (modularity optimization)"
419 }
420
421 fn parameters(&self) -> &[ParameterDef] {
422 louvain_params()
423 }
424
425 fn execute(&self, store: &LpgStore, params: &Parameters) -> Result<AlgorithmResult> {
426 let resolution = params.get_float("resolution").unwrap_or(1.0);
427
428 let result = louvain(store, resolution);
429
430 let mut output = AlgorithmResult::new(vec![
431 "node_id".to_string(),
432 "community_id".to_string(),
433 "modularity".to_string(),
434 ]);
435
436 for (node, community_id) in result.communities {
437 output.add_row(vec![
438 Value::Int64(node.0 as i64),
439 Value::Int64(community_id as i64),
440 Value::Float64(result.modularity),
441 ]);
442 }
443
444 Ok(output)
445 }
446}
447
448#[cfg(test)]
453mod tests {
454 use super::*;
455
456 fn create_two_cliques_graph() -> LpgStore {
457 let store = LpgStore::new();
462
463 let nodes: Vec<NodeId> = (0..8).map(|_| store.create_node(&["Node"])).collect();
464
465 for i in 0..4 {
467 for j in (i + 1)..4 {
468 store.create_edge(nodes[i], nodes[j], "EDGE");
469 store.create_edge(nodes[j], nodes[i], "EDGE");
470 }
471 }
472
473 for i in 4..8 {
475 for j in (i + 1)..8 {
476 store.create_edge(nodes[i], nodes[j], "EDGE");
477 store.create_edge(nodes[j], nodes[i], "EDGE");
478 }
479 }
480
481 store.create_edge(nodes[3], nodes[4], "EDGE");
483 store.create_edge(nodes[4], nodes[3], "EDGE");
484
485 store
486 }
487
488 fn create_simple_graph() -> LpgStore {
489 let store = LpgStore::new();
490
491 let n0 = store.create_node(&["Node"]);
493 let n1 = store.create_node(&["Node"]);
494 let n2 = store.create_node(&["Node"]);
495
496 store.create_edge(n0, n1, "EDGE");
497 store.create_edge(n1, n2, "EDGE");
498
499 store
500 }
501
502 #[test]
503 fn test_label_propagation_basic() {
504 let store = create_simple_graph();
505 let communities = label_propagation(&store, 100);
506
507 assert_eq!(communities.len(), 3);
508
509 for (_, &comm) in &communities {
511 assert!(comm < 3);
512 }
513 }
514
515 #[test]
516 fn test_label_propagation_cliques() {
517 let store = create_two_cliques_graph();
518 let communities = label_propagation(&store, 100);
519
520 assert_eq!(communities.len(), 8);
521
522 let num_comms = community_count(&communities);
524 assert!(num_comms >= 1 && num_comms <= 8); }
526
527 #[test]
528 fn test_label_propagation_empty() {
529 let store = LpgStore::new();
530 let communities = label_propagation(&store, 100);
531 assert!(communities.is_empty());
532 }
533
534 #[test]
535 fn test_label_propagation_single_node() {
536 let store = LpgStore::new();
537 store.create_node(&["Node"]);
538
539 let communities = label_propagation(&store, 100);
540 assert_eq!(communities.len(), 1);
541 }
542
543 #[test]
544 fn test_louvain_basic() {
545 let store = create_simple_graph();
546 let result = louvain(&store, 1.0);
547
548 assert_eq!(result.communities.len(), 3);
549 assert!(result.num_communities >= 1);
550 }
551
552 #[test]
553 fn test_louvain_cliques() {
554 let store = create_two_cliques_graph();
555 let result = louvain(&store, 1.0);
556
557 assert_eq!(result.communities.len(), 8);
558
559 assert!(result.num_communities >= 1 && result.num_communities <= 8);
562 }
563
564 #[test]
565 fn test_louvain_empty() {
566 let store = LpgStore::new();
567 let result = louvain(&store, 1.0);
568
569 assert!(result.communities.is_empty());
570 assert_eq!(result.modularity, 0.0);
571 assert_eq!(result.num_communities, 0);
572 }
573
574 #[test]
575 fn test_louvain_isolated_nodes() {
576 let store = LpgStore::new();
577 store.create_node(&["Node"]);
578 store.create_node(&["Node"]);
579 store.create_node(&["Node"]);
580
581 let result = louvain(&store, 1.0);
582
583 assert_eq!(result.communities.len(), 3);
585 assert_eq!(result.num_communities, 3);
586 }
587
588 #[test]
589 fn test_louvain_resolution_parameter() {
590 let store = create_two_cliques_graph();
591
592 let result_low = louvain(&store, 0.5);
594
595 let result_high = louvain(&store, 2.0);
597
598 assert!(!result_low.communities.is_empty());
600 assert!(!result_high.communities.is_empty());
601 }
602
603 #[test]
604 fn test_community_count() {
605 let mut communities: FxHashMap<NodeId, u64> = FxHashMap::default();
606 communities.insert(NodeId::new(0), 0);
607 communities.insert(NodeId::new(1), 0);
608 communities.insert(NodeId::new(2), 1);
609 communities.insert(NodeId::new(3), 1);
610 communities.insert(NodeId::new(4), 2);
611
612 assert_eq!(community_count(&communities), 3);
613 }
614}