graphos_adapters/plugins/algorithms/
structure.rs1use std::sync::OnceLock;
6
7use graphos_common::types::{NodeId, Value};
8use graphos_common::utils::error::Result;
9use graphos_common::utils::hash::{FxHashMap, FxHashSet};
10use graphos_core::graph::Direction;
11use graphos_core::graph::lpg::LpgStore;
12
13use super::super::{AlgorithmResult, ParameterDef, ParameterType, Parameters};
14use super::traits::GraphAlgorithm;
15
16pub fn articulation_points(store: &LpgStore) -> FxHashSet<NodeId> {
37 let nodes = store.node_ids();
38 let n = nodes.len();
39
40 if n == 0 {
41 return FxHashSet::default();
42 }
43
44 let mut node_to_idx: FxHashMap<NodeId, usize> = FxHashMap::default();
46 let mut idx_to_node: Vec<NodeId> = Vec::with_capacity(n);
47 for (idx, &node) in nodes.iter().enumerate() {
48 node_to_idx.insert(node, idx);
49 idx_to_node.push(node);
50 }
51
52 let mut adj: Vec<FxHashSet<usize>> = vec![FxHashSet::default(); n];
54 for &node in &nodes {
55 let i = *node_to_idx.get(&node).unwrap();
56 for (neighbor, _) in store.edges_from(node, Direction::Outgoing) {
57 if let Some(&j) = node_to_idx.get(&neighbor) {
58 adj[i].insert(j);
59 adj[j].insert(i); }
61 }
62 }
63
64 let mut visited = vec![false; n];
65 let mut disc = vec![0usize; n]; let mut low = vec![0usize; n]; let mut parent = vec![None::<usize>; n];
68 let mut ap = vec![false; n]; let mut time = 0usize;
70
71 for start in 0..n {
73 if visited[start] {
74 continue;
75 }
76
77 let mut stack: Vec<(usize, usize)> = vec![(start, 0)]; let mut children_count: FxHashMap<usize, usize> = FxHashMap::default();
80
81 while let Some(&(u, idx)) = stack.last() {
82 if !visited[u] {
83 visited[u] = true;
84 disc[u] = time;
85 low[u] = time;
86 time += 1;
87 children_count.insert(u, 0);
88 }
89
90 let neighbors: Vec<usize> = adj[u].iter().copied().collect();
91
92 if idx < neighbors.len() {
93 let v = neighbors[idx];
94 stack.last_mut().unwrap().1 += 1;
95
96 if !visited[v] {
97 parent[v] = Some(u);
98 *children_count.entry(u).or_insert(0) += 1;
99 stack.push((v, 0));
100 } else if parent[u] != Some(v) {
101 low[u] = low[u].min(disc[v]);
102 }
103 } else {
104 stack.pop();
105
106 if let Some(p) = parent[u] {
107 low[p] = low[p].min(low[u]);
108
109 if parent[p].is_some() && low[u] >= disc[p] {
111 ap[p] = true;
112 }
113 }
114
115 if parent[u].is_none() && *children_count.get(&u).unwrap_or(&0) > 1 {
117 ap[u] = true;
118 }
119 }
120 }
121 }
122
123 ap.iter()
124 .enumerate()
125 .filter(|&(_, is_ap)| *is_ap)
126 .map(|(idx, _)| idx_to_node[idx])
127 .collect()
128}
129
130pub fn bridges(store: &LpgStore) -> Vec<(NodeId, NodeId)> {
151 let nodes = store.node_ids();
152 let n = nodes.len();
153
154 if n == 0 {
155 return Vec::new();
156 }
157
158 let mut node_to_idx: FxHashMap<NodeId, usize> = FxHashMap::default();
160 let mut idx_to_node: Vec<NodeId> = Vec::with_capacity(n);
161 for (idx, &node) in nodes.iter().enumerate() {
162 node_to_idx.insert(node, idx);
163 idx_to_node.push(node);
164 }
165
166 let mut adj: Vec<FxHashSet<usize>> = vec![FxHashSet::default(); n];
168 for &node in &nodes {
169 let i = *node_to_idx.get(&node).unwrap();
170 for (neighbor, _) in store.edges_from(node, Direction::Outgoing) {
171 if let Some(&j) = node_to_idx.get(&neighbor) {
172 adj[i].insert(j);
173 adj[j].insert(i);
174 }
175 }
176 }
177
178 let mut visited = vec![false; n];
179 let mut disc = vec![0usize; n];
180 let mut low = vec![0usize; n];
181 let mut parent = vec![None::<usize>; n];
182 let mut time = 0usize;
183 let mut bridge_list: Vec<(usize, usize)> = Vec::new();
184
185 for start in 0..n {
186 if visited[start] {
187 continue;
188 }
189
190 let mut stack: Vec<(usize, usize)> = vec![(start, 0)];
191
192 while let Some(&(u, idx)) = stack.last() {
193 if !visited[u] {
194 visited[u] = true;
195 disc[u] = time;
196 low[u] = time;
197 time += 1;
198 }
199
200 let neighbors: Vec<usize> = adj[u].iter().copied().collect();
201
202 if idx < neighbors.len() {
203 let v = neighbors[idx];
204 stack.last_mut().unwrap().1 += 1;
205
206 if !visited[v] {
207 parent[v] = Some(u);
208 stack.push((v, 0));
209 } else if parent[u] != Some(v) {
210 low[u] = low[u].min(disc[v]);
211 }
212 } else {
213 stack.pop();
214
215 if let Some(p) = parent[u] {
216 low[p] = low[p].min(low[u]);
217
218 if low[u] > disc[p] {
220 bridge_list.push((p.min(u), p.max(u)));
221 }
222 }
223 }
224 }
225 }
226
227 bridge_list
228 .into_iter()
229 .map(|(i, j)| (idx_to_node[i], idx_to_node[j]))
230 .collect()
231}
232
233#[derive(Debug, Clone)]
239pub struct KCoreResult {
240 pub core_numbers: FxHashMap<NodeId, usize>,
242 pub max_core: usize,
244}
245
246impl KCoreResult {
247 pub fn k_core(&self, k: usize) -> Vec<NodeId> {
249 self.core_numbers
250 .iter()
251 .filter(|&(_, core)| *core >= k)
252 .map(|(&node, _)| node)
253 .collect()
254 }
255
256 pub fn k_shell(&self, k: usize) -> Vec<NodeId> {
258 self.core_numbers
259 .iter()
260 .filter(|&(_, core)| *core == k)
261 .map(|(&node, _)| node)
262 .collect()
263 }
264}
265
266pub fn kcore_decomposition(store: &LpgStore) -> KCoreResult {
283 let nodes = store.node_ids();
284 let n = nodes.len();
285
286 if n == 0 {
287 return KCoreResult {
288 core_numbers: FxHashMap::default(),
289 max_core: 0,
290 };
291 }
292
293 let mut node_to_idx: FxHashMap<NodeId, usize> = FxHashMap::default();
295 let mut idx_to_node: Vec<NodeId> = Vec::with_capacity(n);
296 for (idx, &node) in nodes.iter().enumerate() {
297 node_to_idx.insert(node, idx);
298 idx_to_node.push(node);
299 }
300
301 let mut adj: Vec<FxHashSet<usize>> = vec![FxHashSet::default(); n];
303 for &node in &nodes {
304 let i = *node_to_idx.get(&node).unwrap();
305 for (neighbor, _) in store.edges_from(node, Direction::Outgoing) {
306 if let Some(&j) = node_to_idx.get(&neighbor) {
307 adj[i].insert(j);
308 adj[j].insert(i);
309 }
310 }
311 }
312
313 let mut degree: Vec<usize> = adj.iter().map(|neighbors| neighbors.len()).collect();
314 let mut core = vec![0usize; n];
315 let mut removed = vec![false; n];
316
317 let max_degree = *degree.iter().max().unwrap_or(&0);
319 if max_degree == 0 {
320 return KCoreResult {
321 core_numbers: nodes.iter().map(|&n| (n, 0)).collect(),
322 max_core: 0,
323 };
324 }
325
326 let mut buckets: Vec<FxHashSet<usize>> = vec![FxHashSet::default(); max_degree + 1];
328 for (i, &d) in degree.iter().enumerate() {
329 buckets[d].insert(i);
330 }
331
332 let mut max_core_val = 0;
333
334 for _ in 0..n {
336 let mut min_deg = 0;
338 while min_deg <= max_degree && buckets[min_deg].is_empty() {
339 min_deg += 1;
340 }
341
342 if min_deg > max_degree {
343 break;
344 }
345
346 let v = *buckets[min_deg].iter().next().unwrap();
348 buckets[min_deg].remove(&v);
349 removed[v] = true;
350 core[v] = min_deg;
351 max_core_val = max_core_val.max(min_deg);
352
353 for &u in &adj[v] {
355 if !removed[u] && degree[u] > 0 {
356 let old_deg = degree[u];
357 buckets[old_deg].remove(&u);
358 degree[u] -= 1;
359 let new_deg = degree[u];
360 buckets[new_deg].insert(u);
361 }
362 }
363 }
364
365 let core_numbers: FxHashMap<NodeId, usize> =
366 (0..n).map(|i| (idx_to_node[i], core[i])).collect();
367
368 KCoreResult {
369 core_numbers,
370 max_core: max_core_val,
371 }
372}
373
374pub fn k_core(store: &LpgStore, k: usize) -> Vec<NodeId> {
376 let result = kcore_decomposition(store);
377 result.k_core(k)
378}
379
380static ARTICULATION_PARAMS: OnceLock<Vec<ParameterDef>> = OnceLock::new();
386
387fn articulation_params() -> &'static [ParameterDef] {
388 ARTICULATION_PARAMS.get_or_init(Vec::new)
389}
390
391pub struct ArticulationPointsAlgorithm;
393
394impl GraphAlgorithm for ArticulationPointsAlgorithm {
395 fn name(&self) -> &str {
396 "articulation_points"
397 }
398
399 fn description(&self) -> &str {
400 "Find articulation points (cut vertices) in the graph"
401 }
402
403 fn parameters(&self) -> &[ParameterDef] {
404 articulation_params()
405 }
406
407 fn execute(&self, store: &LpgStore, _params: &Parameters) -> Result<AlgorithmResult> {
408 let points = articulation_points(store);
409
410 let mut result = AlgorithmResult::new(vec!["node_id".to_string()]);
411
412 for node in points {
413 result.add_row(vec![Value::Int64(node.0 as i64)]);
414 }
415
416 Ok(result)
417 }
418}
419
420static BRIDGES_PARAMS: OnceLock<Vec<ParameterDef>> = OnceLock::new();
422
423fn bridges_params() -> &'static [ParameterDef] {
424 BRIDGES_PARAMS.get_or_init(Vec::new)
425}
426
427pub struct BridgesAlgorithm;
429
430impl GraphAlgorithm for BridgesAlgorithm {
431 fn name(&self) -> &str {
432 "bridges"
433 }
434
435 fn description(&self) -> &str {
436 "Find bridges (cut edges) in the graph"
437 }
438
439 fn parameters(&self) -> &[ParameterDef] {
440 bridges_params()
441 }
442
443 fn execute(&self, store: &LpgStore, _params: &Parameters) -> Result<AlgorithmResult> {
444 let bridge_list = bridges(store);
445
446 let mut result = AlgorithmResult::new(vec!["source".to_string(), "target".to_string()]);
447
448 for (src, dst) in bridge_list {
449 result.add_row(vec![Value::Int64(src.0 as i64), Value::Int64(dst.0 as i64)]);
450 }
451
452 Ok(result)
453 }
454}
455
456static KCORE_PARAMS: OnceLock<Vec<ParameterDef>> = OnceLock::new();
458
459fn kcore_params() -> &'static [ParameterDef] {
460 KCORE_PARAMS.get_or_init(|| {
461 vec![ParameterDef {
462 name: "k".to_string(),
463 description: "Core number threshold (optional, returns decomposition if not set)"
464 .to_string(),
465 param_type: ParameterType::Integer,
466 required: false,
467 default: None,
468 }]
469 })
470}
471
472pub struct KCoreAlgorithm;
474
475impl GraphAlgorithm for KCoreAlgorithm {
476 fn name(&self) -> &str {
477 "kcore"
478 }
479
480 fn description(&self) -> &str {
481 "K-core decomposition of the graph"
482 }
483
484 fn parameters(&self) -> &[ParameterDef] {
485 kcore_params()
486 }
487
488 fn execute(&self, store: &LpgStore, params: &Parameters) -> Result<AlgorithmResult> {
489 let decomposition = kcore_decomposition(store);
490
491 if let Some(k) = params.get_int("k") {
492 let k_core_nodes = decomposition.k_core(k as usize);
494
495 let mut result =
496 AlgorithmResult::new(vec!["node_id".to_string(), "in_k_core".to_string()]);
497
498 for node in k_core_nodes {
499 result.add_row(vec![Value::Int64(node.0 as i64), Value::Bool(true)]);
500 }
501
502 Ok(result)
503 } else {
504 let mut result = AlgorithmResult::new(vec![
506 "node_id".to_string(),
507 "core_number".to_string(),
508 "max_core".to_string(),
509 ]);
510
511 for (node, core) in decomposition.core_numbers {
512 result.add_row(vec![
513 Value::Int64(node.0 as i64),
514 Value::Int64(core as i64),
515 Value::Int64(decomposition.max_core as i64),
516 ]);
517 }
518
519 Ok(result)
520 }
521 }
522}
523
524#[cfg(test)]
529mod tests {
530 use super::*;
531
532 fn create_simple_path() -> LpgStore {
533 let store = LpgStore::new();
535
536 let n0 = store.create_node(&["Node"]);
537 let n1 = store.create_node(&["Node"]);
538 let n2 = store.create_node(&["Node"]);
539 let n3 = store.create_node(&["Node"]);
540
541 store.create_edge(n0, n1, "EDGE");
542 store.create_edge(n1, n0, "EDGE");
543 store.create_edge(n1, n2, "EDGE");
544 store.create_edge(n2, n1, "EDGE");
545 store.create_edge(n2, n3, "EDGE");
546 store.create_edge(n3, n2, "EDGE");
547
548 store
549 }
550
551 fn create_diamond() -> LpgStore {
552 let store = LpgStore::new();
557
558 let n0 = store.create_node(&["Node"]);
559 let n1 = store.create_node(&["Node"]);
560 let n2 = store.create_node(&["Node"]);
561 let n3 = store.create_node(&["Node"]);
562
563 store.create_edge(n0, n1, "EDGE");
565 store.create_edge(n1, n0, "EDGE");
566 store.create_edge(n0, n2, "EDGE");
567 store.create_edge(n2, n0, "EDGE");
568 store.create_edge(n1, n3, "EDGE");
569 store.create_edge(n3, n1, "EDGE");
570 store.create_edge(n2, n3, "EDGE");
571 store.create_edge(n3, n2, "EDGE");
572
573 store
574 }
575
576 fn create_tree() -> LpgStore {
577 let store = LpgStore::new();
583
584 let n0 = store.create_node(&["Node"]);
585 let n1 = store.create_node(&["Node"]);
586 let n2 = store.create_node(&["Node"]);
587 let n3 = store.create_node(&["Node"]);
588
589 store.create_edge(n0, n1, "EDGE");
590 store.create_edge(n1, n0, "EDGE");
591 store.create_edge(n0, n2, "EDGE");
592 store.create_edge(n2, n0, "EDGE");
593 store.create_edge(n1, n3, "EDGE");
594 store.create_edge(n3, n1, "EDGE");
595
596 store
597 }
598
599 #[test]
600 fn test_articulation_points_path() {
601 let store = create_simple_path();
602 let ap = articulation_points(&store);
603
604 assert!(ap.len() >= 2);
607 assert!(ap.contains(&NodeId::new(1)) || ap.contains(&NodeId::new(2)));
608 }
609
610 #[test]
611 fn test_articulation_points_diamond() {
612 let store = create_diamond();
613 let ap = articulation_points(&store);
614
615 assert!(ap.is_empty());
617 }
618
619 #[test]
620 fn test_articulation_points_tree() {
621 let store = create_tree();
622 let ap = articulation_points(&store);
623
624 assert!(ap.contains(&NodeId::new(0)) || ap.contains(&NodeId::new(1)));
628 }
629
630 #[test]
631 fn test_articulation_points_empty() {
632 let store = LpgStore::new();
633 let ap = articulation_points(&store);
634 assert!(ap.is_empty());
635 }
636
637 #[test]
638 fn test_bridges_path() {
639 let store = create_simple_path();
640 let br = bridges(&store);
641
642 assert_eq!(br.len(), 3);
644 }
645
646 #[test]
647 fn test_bridges_diamond() {
648 let store = create_diamond();
649 let br = bridges(&store);
650
651 assert!(br.is_empty());
653 }
654
655 #[test]
656 fn test_bridges_empty() {
657 let store = LpgStore::new();
658 let br = bridges(&store);
659 assert!(br.is_empty());
660 }
661
662 #[test]
663 fn test_kcore_path() {
664 let store = create_simple_path();
665 let result = kcore_decomposition(&store);
666
667 let max_core = result.core_numbers.values().copied().max().unwrap_or(0);
670 assert!(max_core >= 1);
671 assert_eq!(result.max_core, max_core);
672 }
673
674 #[test]
675 fn test_kcore_triangle() {
676 let store = LpgStore::new();
677 let n0 = store.create_node(&["Node"]);
678 let n1 = store.create_node(&["Node"]);
679 let n2 = store.create_node(&["Node"]);
680
681 store.create_edge(n0, n1, "EDGE");
682 store.create_edge(n1, n0, "EDGE");
683 store.create_edge(n1, n2, "EDGE");
684 store.create_edge(n2, n1, "EDGE");
685 store.create_edge(n0, n2, "EDGE");
686 store.create_edge(n2, n0, "EDGE");
687
688 let result = kcore_decomposition(&store);
689
690 assert!(result.max_core >= 1);
692 assert_eq!(result.core_numbers.len(), 3);
694 }
695
696 #[test]
697 fn test_kcore_empty() {
698 let store = LpgStore::new();
699 let result = kcore_decomposition(&store);
700
701 assert!(result.core_numbers.is_empty());
702 assert_eq!(result.max_core, 0);
703 }
704
705 #[test]
706 fn test_kcore_isolated() {
707 let store = LpgStore::new();
708 store.create_node(&["Node"]);
709 store.create_node(&["Node"]);
710
711 let result = kcore_decomposition(&store);
712
713 for (_, &core) in &result.core_numbers {
715 assert_eq!(core, 0);
716 }
717 }
718
719 #[test]
720 fn test_k_core_extraction() {
721 let store = create_simple_path();
722 let result = kcore_decomposition(&store);
723
724 let k0_core = result.k_core(0);
726 assert_eq!(k0_core.len(), 4);
727
728 let k1_core = result.k_core(1);
730 assert!(k1_core.len() <= 4);
731
732 let k2_core = result.k_core(2);
733 assert!(k2_core.len() <= k1_core.len());
734 }
735
736 #[test]
737 fn test_k_shell() {
738 let store = create_simple_path();
739 let result = kcore_decomposition(&store);
740
741 let total_in_shells: usize = (0..=result.max_core).map(|k| result.k_shell(k).len()).sum();
743 assert_eq!(total_in_shells, 4);
744 }
745}