1use std::collections::HashMap;
13
14use super::graph::Graph;
15
16pub const COMMUNITY_COLORS: [&str; 8] = [
19 "\x1b[31m", "\x1b[32m", "\x1b[33m", "\x1b[34m", "\x1b[35m", "\x1b[36m", "\x1b[91m", "\x1b[92m", ];
28
29pub struct GraphAnalytics;
31
32impl GraphAnalytics {
33 pub fn pagerank<N, E>(
45 graph: &Graph<N, E>,
46 damping: f32,
47 iterations: usize,
48 ) -> HashMap<String, f32> {
49 let n = graph.node_count();
50 if n == 0 {
51 return HashMap::new();
52 }
53
54 let teleport = (1.0 - damping) / n as f32;
55 let node_ids: Vec<String> = graph.nodes.keys().cloned().collect();
56
57 let mut ranks: HashMap<String, f32> =
59 node_ids.iter().map(|id| (id.clone(), 1.0 / n as f32)).collect();
60
61 let out_degrees: HashMap<String, usize> =
63 node_ids.iter().map(|id| (id.clone(), graph.neighbors(id).len())).collect();
64
65 for _ in 0..iterations {
67 let mut new_ranks: HashMap<String, f32> =
68 node_ids.iter().map(|id| (id.clone(), teleport)).collect();
69
70 for edge in graph.edges() {
72 let out_deg = *out_degrees.get(&edge.from).unwrap_or(&1);
73 if out_deg > 0 {
74 let contribution =
75 damping * ranks.get(&edge.from).unwrap_or(&0.0) / out_deg as f32;
76 *new_ranks.entry(edge.to.clone()).or_default() += contribution;
77 }
78 }
79
80 let dangling_sum: f32 = node_ids
82 .iter()
83 .filter(|id| *out_degrees.get(*id).unwrap_or(&0) == 0)
84 .map(|id| ranks.get(id).unwrap_or(&0.0))
85 .sum();
86
87 let dangling_contribution = damping * dangling_sum / n as f32;
88 for rank in new_ranks.values_mut() {
89 *rank += dangling_contribution;
90 }
91
92 ranks = new_ranks;
93 }
94
95 let max_rank = ranks.values().copied().fold(0.0_f32, f32::max);
97 if max_rank > 0.0 {
98 for rank in ranks.values_mut() {
99 *rank /= max_rank;
100 }
101 }
102
103 ranks
104 }
105
106 pub fn apply_pagerank<N, E>(graph: &mut Graph<N, E>, damping: f32, iterations: usize) {
108 let ranks = Self::pagerank(graph, damping, iterations);
109 for (id, rank) in ranks {
110 if let Some(node) = graph.get_node_mut(&id) {
111 node.importance = rank;
112 }
113 }
114 }
115
116 pub fn detect_communities<N, E>(graph: &Graph<N, E>) -> HashMap<String, usize> {
126 let node_ids: Vec<String> = graph.nodes.keys().cloned().collect();
127 let n = node_ids.len();
128
129 if n == 0 {
130 return HashMap::new();
131 }
132
133 let mut communities: HashMap<String, usize> =
135 node_ids.iter().enumerate().map(|(i, id)| (id.clone(), i)).collect();
136
137 let mut adjacency: HashMap<String, Vec<String>> = HashMap::new();
139 for edge in graph.edges() {
140 adjacency.entry(edge.from.clone()).or_default().push(edge.to.clone());
141 adjacency.entry(edge.to.clone()).or_default().push(edge.from.clone());
143 }
144
145 let mut improved = true;
147 let mut max_iterations = 10;
148
149 while improved && max_iterations > 0 {
150 improved = false;
151 max_iterations -= 1;
152
153 for node_id in &node_ids {
154 let current_comm = *communities.get(node_id).unwrap_or(&0);
155
156 let neighbors = adjacency.get(node_id).cloned().unwrap_or_default();
158 let mut neighbor_comms: HashMap<usize, usize> = HashMap::new();
159
160 for neighbor in &neighbors {
161 let comm = *communities.get(neighbor).unwrap_or(&0);
162 *neighbor_comms.entry(comm).or_default() += 1;
163 }
164
165 if let Some((&best_comm, &count)) = neighbor_comms.iter().max_by_key(|(_, &c)| c) {
167 if best_comm != current_comm && count > 1 {
168 communities.insert(node_id.clone(), best_comm);
169 improved = true;
170 }
171 }
172 }
173 }
174
175 let mut comm_map: HashMap<usize, usize> = HashMap::new();
177 let mut next_comm = 0;
178
179 for comm in communities.values_mut() {
180 let new_comm = *comm_map.entry(*comm).or_insert_with(|| {
181 let c = next_comm;
182 next_comm += 1;
183 c
184 });
185 *comm = new_comm;
186 }
187
188 communities
189 }
190
191 pub fn apply_communities<N, E>(graph: &mut Graph<N, E>) -> usize {
193 let communities = Self::detect_communities(graph);
194 let num_communities = communities.values().max().map(|&m| m + 1).unwrap_or(0);
195
196 for (id, comm) in &communities {
198 if let Some(node) = graph.get_node_mut(id) {
199 let comm_normalized =
200 if num_communities > 0 { *comm as f32 / num_communities as f32 } else { 0.0 };
201 node.importance = f32::midpoint(node.importance, comm_normalized);
202 }
203 }
204
205 num_communities
206 }
207
208 #[must_use]
210 pub fn community_color(community_id: usize) -> &'static str {
211 COMMUNITY_COLORS[community_id % COMMUNITY_COLORS.len()]
212 }
213
214 #[must_use]
218 pub fn degree_centrality<N, E>(graph: &Graph<N, E>) -> HashMap<String, f32> {
219 let n = graph.node_count();
220 if n <= 1 {
221 return graph.nodes.keys().map(|id| (id.clone(), 0.0)).collect();
222 }
223
224 let mut in_degree: HashMap<String, usize> = HashMap::new();
225 let mut out_degree: HashMap<String, usize> = HashMap::new();
226
227 for id in graph.nodes.keys() {
228 in_degree.insert(id.clone(), 0);
229 out_degree.insert(id.clone(), 0);
230 }
231
232 for edge in &graph.edges {
233 *out_degree.entry(edge.from.clone()).or_default() += 1;
234 *in_degree.entry(edge.to.clone()).or_default() += 1;
235 }
236
237 graph
238 .nodes
239 .keys()
240 .map(|id| {
241 let total = in_degree.get(id).unwrap_or(&0) + out_degree.get(id).unwrap_or(&0);
242 (id.clone(), total as f32 / (n - 1) as f32)
243 })
244 .collect()
245 }
246
247 #[must_use]
251 pub fn betweenness_centrality<N, E>(graph: &Graph<N, E>) -> HashMap<String, f32> {
252 let n = graph.node_count();
253 if n <= 2 {
254 return graph.nodes.keys().map(|id| (id.clone(), 0.0)).collect();
255 }
256
257 let mut betweenness: HashMap<String, f32> =
258 graph.nodes.keys().map(|id| (id.clone(), 0.0)).collect();
259 let node_ids: Vec<String> = graph.nodes.keys().cloned().collect();
260
261 let mut adj: HashMap<String, Vec<String>> = HashMap::new();
263 for id in &node_ids {
264 adj.insert(id.clone(), Vec::new());
265 }
266 for edge in &graph.edges {
267 adj.entry(edge.from.clone()).or_default().push(edge.to.clone());
268 }
269
270 for s in &node_ids {
272 let mut stack: Vec<String> = Vec::new();
273 let mut pred: HashMap<String, Vec<String>> = HashMap::new();
274 let mut sigma: HashMap<String, f32> = HashMap::new();
275 let mut dist: HashMap<String, i32> = HashMap::new();
276
277 for id in &node_ids {
278 pred.insert(id.clone(), Vec::new());
279 sigma.insert(id.clone(), 0.0);
280 dist.insert(id.clone(), -1);
281 }
282 sigma.insert(s.clone(), 1.0);
283 dist.insert(s.clone(), 0);
284
285 let mut queue = std::collections::VecDeque::new();
286 queue.push_back(s.clone());
287
288 while let Some(v) = queue.pop_front() {
289 stack.push(v.clone());
290 let v_dist = *dist.get(&v).unwrap_or(&0);
291
292 for w in adj.get(&v).unwrap_or(&Vec::new()) {
293 let w_dist = dist.get(w).unwrap_or(&-1);
294 if *w_dist < 0 {
295 queue.push_back(w.clone());
296 dist.insert(w.clone(), v_dist + 1);
297 }
298 if *dist.get(w).unwrap_or(&0) == v_dist + 1 {
299 let sigma_v = *sigma.get(&v).unwrap_or(&0.0);
300 *sigma.entry(w.clone()).or_default() += sigma_v;
301 pred.entry(w.clone()).or_default().push(v.clone());
302 }
303 }
304 }
305
306 let mut delta: HashMap<String, f32> =
307 node_ids.iter().map(|id| (id.clone(), 0.0)).collect();
308 while let Some(w) = stack.pop() {
309 for v in pred.get(&w).unwrap_or(&Vec::new()) {
310 let sigma_v = sigma.get(v).unwrap_or(&1.0);
311 let sigma_w = sigma.get(&w).unwrap_or(&1.0);
312 let delta_w = delta.get(&w).unwrap_or(&0.0);
313 *delta.entry(v.clone()).or_default() += (sigma_v / sigma_w) * (1.0 + delta_w);
314 }
315 if &w != s {
316 *betweenness.entry(w.clone()).or_default() += delta.get(&w).unwrap_or(&0.0);
317 }
318 }
319 }
320
321 let norm = if n > 2 { 2.0 / ((n - 1) * (n - 2)) as f32 } else { 1.0 };
323 for val in betweenness.values_mut() {
324 *val *= norm;
325 }
326
327 betweenness
328 }
329
330 #[must_use]
334 pub fn closeness_centrality<N, E>(graph: &Graph<N, E>) -> HashMap<String, f32> {
335 let n = graph.node_count();
336 if n <= 1 {
337 return graph.nodes.keys().map(|id| (id.clone(), 0.0)).collect();
338 }
339
340 let node_ids: Vec<String> = graph.nodes.keys().cloned().collect();
341
342 node_ids
343 .iter()
344 .map(|source| {
345 let distances = Self::bfs_distances(graph, source);
346 let reachable: Vec<i32> = distances.values().filter(|&&d| d > 0).copied().collect();
347
348 let closeness = if reachable.is_empty() {
349 0.0
350 } else {
351 let sum_dist: i32 = reachable.iter().sum();
352 reachable.len() as f32 / sum_dist as f32
353 };
354
355 (source.clone(), closeness)
356 })
357 .collect()
358 }
359
360 #[must_use]
362 pub fn shortest_path<N, E>(graph: &Graph<N, E>, from: &str, to: &str) -> Option<Vec<String>> {
363 if from == to {
364 return Some(vec![from.to_string()]);
365 }
366
367 if !graph.nodes.contains_key(from) || !graph.nodes.contains_key(to) {
368 return None;
369 }
370
371 let mut visited: HashMap<String, String> = HashMap::new();
372 let mut queue = std::collections::VecDeque::new();
373 queue.push_back(from.to_string());
374 visited.insert(from.to_string(), String::new());
375
376 while let Some(current) = queue.pop_front() {
377 for neighbor in graph.neighbors(¤t) {
378 if !visited.contains_key(neighbor) {
379 visited.insert(neighbor.to_string(), current.clone());
380 if neighbor == to {
381 let mut path = vec![to.to_string()];
382 let mut node = to.to_string();
383 while let Some(pred) = visited.get(&node) {
384 if pred.is_empty() {
385 break;
386 }
387 path.push(pred.clone());
388 node = pred.clone();
389 }
390 path.reverse();
391 return Some(path);
392 }
393 queue.push_back(neighbor.to_string());
394 }
395 }
396 }
397
398 None
399 }
400
401 fn bfs_distances<N, E>(graph: &Graph<N, E>, source: &str) -> HashMap<String, i32> {
403 let mut dist: HashMap<String, i32> =
404 graph.nodes.keys().map(|id| (id.clone(), -1)).collect();
405 dist.insert(source.to_string(), 0);
406
407 let mut queue = std::collections::VecDeque::new();
408 queue.push_back(source.to_string());
409
410 while let Some(current) = queue.pop_front() {
411 let current_dist = *dist.get(¤t).unwrap_or(&0);
412 for neighbor in graph.neighbors(¤t) {
413 if *dist.get(neighbor).unwrap_or(&-1) < 0 {
414 dist.insert(neighbor.to_string(), current_dist + 1);
415 queue.push_back(neighbor.to_string());
416 }
417 }
418 }
419
420 dist
421 }
422
423 #[must_use]
425 pub fn is_connected<N, E>(graph: &Graph<N, E>) -> bool {
426 if graph.node_count() == 0 {
427 return true;
428 }
429
430 let mut adj: HashMap<String, Vec<String>> = HashMap::new();
431 for id in graph.nodes.keys() {
432 adj.insert(id.clone(), Vec::new());
433 }
434 for edge in &graph.edges {
435 adj.entry(edge.from.clone()).or_default().push(edge.to.clone());
436 adj.entry(edge.to.clone()).or_default().push(edge.from.clone());
437 }
438
439 let first = graph.nodes.keys().next().expect("graph is non-empty after check");
440 let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
441 let mut queue = std::collections::VecDeque::new();
442 queue.push_back(first.clone());
443 visited.insert(first.clone());
444
445 while let Some(current) = queue.pop_front() {
446 for neighbor in adj.get(¤t).unwrap_or(&Vec::new()) {
447 if !visited.contains(neighbor) {
448 visited.insert(neighbor.clone());
449 queue.push_back(neighbor.clone());
450 }
451 }
452 }
453
454 visited.len() == graph.node_count()
455 }
456}
457
458pub trait GraphAnalyticsExt<N, E> {
460 fn compute_pagerank(&mut self, damping: f32, iterations: usize);
462
463 fn detect_communities(&mut self) -> usize;
465
466 fn pagerank_scores(&self) -> HashMap<String, f32>;
468}
469
470impl<N, E> GraphAnalyticsExt<N, E> for Graph<N, E> {
471 fn compute_pagerank(&mut self, damping: f32, iterations: usize) {
472 GraphAnalytics::apply_pagerank(self, damping, iterations);
473 }
474
475 fn detect_communities(&mut self) -> usize {
476 GraphAnalytics::apply_communities(self)
477 }
478
479 fn pagerank_scores(&self) -> HashMap<String, f32> {
480 GraphAnalytics::pagerank(self, 0.85, 20)
481 }
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487 use crate::tui::graph::Node;
488
489 #[test]
490 fn test_pagerank_empty_graph() {
491 let graph: Graph<(), ()> = Graph::new();
492 let ranks = GraphAnalytics::pagerank(&graph, 0.85, 20);
493 assert!(ranks.is_empty());
494 }
495
496 #[test]
497 fn test_pagerank_single_node() {
498 let mut graph: Graph<(), ()> = Graph::new();
499 graph.add_node(Node::new("a", ()));
500 let ranks = GraphAnalytics::pagerank(&graph, 0.85, 20);
501 assert_eq!(ranks.len(), 1);
502 assert!(ranks.contains_key("a"));
503 }
504
505 #[test]
506 fn test_community_detection_empty() {
507 let graph: Graph<(), ()> = Graph::new();
508 let communities = GraphAnalytics::detect_communities(&graph);
509 assert!(communities.is_empty());
510 }
511
512 #[test]
513 fn test_is_connected_empty() {
514 let graph: Graph<(), ()> = Graph::new();
515 assert!(GraphAnalytics::is_connected(&graph));
516 }
517
518 #[test]
519 fn test_community_color() {
520 let color = GraphAnalytics::community_color(0);
521 assert_eq!(color, "\x1b[31m"); }
523
524 #[test]
525 fn test_community_color_wraps() {
526 let c0 = GraphAnalytics::community_color(0);
528 let c8 = GraphAnalytics::community_color(8);
529 assert_eq!(c0, c8); }
531
532 #[test]
533 fn test_degree_centrality_empty() {
534 let graph: Graph<(), ()> = Graph::new();
535 let centrality = GraphAnalytics::degree_centrality(&graph);
536 assert!(centrality.is_empty());
537 }
538
539 #[test]
540 fn test_degree_centrality_single_node() {
541 let mut graph: Graph<(), ()> = Graph::new();
542 graph.add_node(Node::new("a", ()));
543 let centrality = GraphAnalytics::degree_centrality(&graph);
544 assert_eq!(centrality.len(), 1);
545 assert_eq!(*centrality.get("a").expect("key not found"), 0.0);
546 }
547
548 #[test]
549 fn test_betweenness_centrality_empty() {
550 let graph: Graph<(), ()> = Graph::new();
551 let centrality = GraphAnalytics::betweenness_centrality(&graph);
552 assert!(centrality.is_empty());
553 }
554
555 #[test]
556 fn test_closeness_centrality_empty() {
557 let graph: Graph<(), ()> = Graph::new();
558 let centrality = GraphAnalytics::closeness_centrality(&graph);
559 assert!(centrality.is_empty());
560 }
561
562 #[test]
563 fn test_closeness_centrality_single_node() {
564 let mut graph: Graph<(), ()> = Graph::new();
565 graph.add_node(Node::new("a", ()));
566 let centrality = GraphAnalytics::closeness_centrality(&graph);
567 assert_eq!(centrality.len(), 1);
568 assert_eq!(*centrality.get("a").expect("key not found"), 0.0);
569 }
570
571 #[test]
572 fn test_shortest_path_same_node() {
573 let mut graph: Graph<(), ()> = Graph::new();
574 graph.add_node(Node::new("a", ()));
575 let path = GraphAnalytics::shortest_path(&graph, "a", "a");
576 assert_eq!(path, Some(vec!["a".to_string()]));
577 }
578
579 #[test]
580 fn test_shortest_path_nonexistent() {
581 let graph: Graph<(), ()> = Graph::new();
582 let path = GraphAnalytics::shortest_path(&graph, "a", "b");
583 assert!(path.is_none());
584 }
585
586 #[test]
587 fn test_is_connected_single_node() {
588 let mut graph: Graph<(), ()> = Graph::new();
589 graph.add_node(Node::new("a", ()));
590 assert!(GraphAnalytics::is_connected(&graph));
591 }
592
593 #[test]
594 fn test_graph_analytics_ext_trait() {
595 use crate::tui::graph::Edge;
596
597 let mut graph: Graph<(), ()> = Graph::new();
598 graph.add_node(Node::new("a", ()));
599 graph.add_node(Node::new("b", ()));
600 graph.add_edge(Edge::new("a", "b", ()));
601
602 graph.compute_pagerank(0.85, 10);
604 let num_communities = graph.detect_communities();
605 let scores = graph.pagerank_scores();
606
607 assert!(scores.contains_key("a"));
608 let _ = num_communities; }
610
611 #[test]
612 fn test_apply_pagerank() {
613 use crate::tui::graph::Edge;
614
615 let mut graph: Graph<(), ()> = Graph::new();
616 graph.add_node(Node::new("a", ()));
617 graph.add_node(Node::new("b", ()));
618 graph.add_edge(Edge::new("a", "b", ()));
619
620 GraphAnalytics::apply_pagerank(&mut graph, 0.85, 10);
621
622 assert!(graph.get_node("a").expect("unexpected failure").importance >= 0.0);
624 assert!(graph.get_node("b").expect("unexpected failure").importance >= 0.0);
625 }
626
627 #[test]
628 fn test_apply_communities() {
629 use crate::tui::graph::Edge;
630
631 let mut graph: Graph<(), ()> = Graph::new();
632 graph.add_node(Node::new("a", ()));
633 graph.add_node(Node::new("b", ()));
634 graph.add_edge(Edge::new("a", "b", ()));
635
636 let num_communities = GraphAnalytics::apply_communities(&mut graph);
637 assert!(num_communities >= 1);
638 }
639}