1use serde::{Deserialize, Serialize};
11use std::collections::{HashMap, HashSet, VecDeque};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Community {
16 pub id: usize,
18 pub nodes: Vec<String>,
20 pub modularity: f32,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct CentralityScores {
27 pub node_id: String,
29 pub degree: f32,
31 pub betweenness: f32,
33 pub closeness: f32,
35 pub pagerank: Option<f32>,
37}
38
39#[derive(Debug, Clone)]
41pub struct Path {
42 pub nodes: Vec<String>,
44 pub weight: f32,
46}
47
48struct PathSearchState<'a> {
50 path: &'a mut Vec<String>,
51 visited: &'a mut HashSet<String>,
52 all_paths: &'a mut Vec<Path>,
53 weight: f32,
54}
55
56pub struct GraphAnalytics {
58 adjacency: HashMap<String, Vec<(String, f32)>>,
60 degrees: HashMap<String, usize>,
62}
63
64impl GraphAnalytics {
65 pub fn new(edges: Vec<(String, String, f32)>) -> Self {
70 let mut adjacency: HashMap<String, Vec<(String, f32)>> = HashMap::new();
71 let mut degrees: HashMap<String, usize> = HashMap::new();
72
73 for (source, target, weight) in edges {
74 adjacency
75 .entry(source.clone())
76 .or_default()
77 .push((target.clone(), weight));
78
79 adjacency
80 .entry(target.clone())
81 .or_default()
82 .push((source.clone(), weight));
83
84 *degrees.entry(source).or_insert(0) += 1;
85 *degrees.entry(target).or_insert(0) += 1;
86 }
87
88 Self { adjacency, degrees }
89 }
90
91 pub fn detect_communities(&self) -> Vec<Community> {
98 let nodes: Vec<String> = self.adjacency.keys().cloned().collect();
99 let mut communities: HashMap<String, usize> = HashMap::new();
100 let mut community_id = 0;
101
102 for node in &nodes {
104 if !communities.contains_key(node) {
105 let component = self.get_connected_component(node);
106 for n in component {
107 communities.insert(n, community_id);
108 }
109 community_id += 1;
110 }
111 }
112
113 let mut community_map: HashMap<usize, Vec<String>> = HashMap::new();
115 for (node, id) in communities {
116 community_map.entry(id).or_default().push(node);
117 }
118
119 community_map
121 .into_iter()
122 .map(|(id, nodes)| {
123 let modularity = self.calculate_modularity(&nodes);
124 Community {
125 id,
126 nodes,
127 modularity,
128 }
129 })
130 .collect()
131 }
132
133 fn get_connected_component(&self, start: &str) -> Vec<String> {
135 let mut visited = HashSet::new();
136 let mut queue = VecDeque::new();
137 queue.push_back(start.to_string());
138
139 while let Some(node) = queue.pop_front() {
140 if visited.contains(&node) {
141 continue;
142 }
143 visited.insert(node.clone());
144
145 if let Some(neighbors) = self.adjacency.get(&node) {
146 for (neighbor, _) in neighbors {
147 if !visited.contains(neighbor) {
148 queue.push_back(neighbor.clone());
149 }
150 }
151 }
152 }
153
154 visited.into_iter().collect()
155 }
156
157 fn calculate_modularity(&self, nodes: &[String]) -> f32 {
159 let total_edges = self.adjacency.len() as f32;
160 let mut internal_edges = 0.0;
161
162 let node_set: HashSet<_> = nodes.iter().collect();
163
164 for node in nodes {
165 if let Some(neighbors) = self.adjacency.get(node) {
166 for (neighbor, _) in neighbors {
167 if node_set.contains(&neighbor) {
168 internal_edges += 1.0;
169 }
170 }
171 }
172 }
173
174 internal_edges / (2.0 * total_edges)
176 }
177
178 pub fn calculate_centrality(&self) -> HashMap<String, CentralityScores> {
183 let nodes: Vec<String> = self.adjacency.keys().cloned().collect();
184 let n = nodes.len() as f32;
185
186 let mut scores = HashMap::new();
187
188 for node in &nodes {
189 let degree = self.degree_centrality(node, n);
190 let betweenness = self.betweenness_centrality(node);
191 let closeness = self.closeness_centrality(node);
192
193 scores.insert(
194 node.clone(),
195 CentralityScores {
196 node_id: node.clone(),
197 degree,
198 betweenness,
199 closeness,
200 pagerank: None, },
202 );
203 }
204
205 scores
206 }
207
208 fn degree_centrality(&self, node: &str, n: f32) -> f32 {
210 let degree = *self.degrees.get(node).unwrap_or(&0) as f32;
211 if n > 1.0 {
212 degree / (n - 1.0)
213 } else {
214 0.0
215 }
216 }
217
218 fn betweenness_centrality(&self, node: &str) -> f32 {
220 let nodes: Vec<String> = self.adjacency.keys().cloned().collect();
221 let mut betweenness = 0.0;
222
223 for source in &nodes {
225 if source == node {
226 continue;
227 }
228 for target in &nodes {
229 if target == node || source == target {
230 continue;
231 }
232
233 if let Some(path) = self.shortest_path(source, target) {
234 if path.nodes.contains(&node.to_string()) {
235 betweenness += 1.0;
236 }
237 }
238 }
239 }
240
241 let n = nodes.len() as f32;
242 if n > 2.0 {
243 betweenness / ((n - 1.0) * (n - 2.0) / 2.0)
244 } else {
245 0.0
246 }
247 }
248
249 fn closeness_centrality(&self, node: &str) -> f32 {
251 let nodes: Vec<String> = self.adjacency.keys().cloned().collect();
252 let mut total_distance = 0.0;
253 let mut reachable = 0;
254
255 for target in &nodes {
256 if target == node {
257 continue;
258 }
259
260 if let Some(path) = self.shortest_path(node, target) {
261 total_distance += path.weight;
262 reachable += 1;
263 }
264 }
265
266 if reachable > 0 && total_distance > 0.0 {
267 (reachable as f32) / total_distance
268 } else {
269 0.0
270 }
271 }
272
273 pub fn shortest_path(&self, start: &str, end: &str) -> Option<Path> {
282 let mut distances: HashMap<String, f32> = HashMap::new();
283 let mut previous: HashMap<String, String> = HashMap::new();
284 let mut unvisited: HashSet<String> = self.adjacency.keys().cloned().collect();
285
286 distances.insert(start.to_string(), 0.0);
287
288 while !unvisited.is_empty() {
289 let current = unvisited
291 .iter()
292 .min_by(|a, b| {
293 let dist_a = *distances.get(*a).unwrap_or(&f32::INFINITY);
294 let dist_b = *distances.get(*b).unwrap_or(&f32::INFINITY);
295 dist_a.partial_cmp(&dist_b).unwrap()
296 })?
297 .clone();
298
299 if current == end {
300 break;
301 }
302
303 unvisited.remove(¤t);
304
305 let current_dist = *distances.get(¤t).unwrap_or(&f32::INFINITY);
306
307 if let Some(neighbors) = self.adjacency.get(¤t) {
308 for (neighbor, weight) in neighbors {
309 if unvisited.contains(neighbor) {
310 let alt = current_dist + weight;
311 let neighbor_dist = *distances.get(neighbor).unwrap_or(&f32::INFINITY);
312
313 if alt < neighbor_dist {
314 distances.insert(neighbor.clone(), alt);
315 previous.insert(neighbor.clone(), current.clone());
316 }
317 }
318 }
319 }
320 }
321
322 let mut path_nodes = Vec::new();
324 let mut current = end.to_string();
325
326 while let Some(prev) = previous.get(¤t) {
327 path_nodes.push(current.clone());
328 current = prev.clone();
329 }
330
331 if current == start {
332 path_nodes.push(start.to_string());
333 path_nodes.reverse();
334
335 let weight = *distances.get(end).unwrap_or(&f32::INFINITY);
336
337 Some(Path {
338 nodes: path_nodes,
339 weight,
340 })
341 } else {
342 None
343 }
344 }
345
346 pub fn all_paths(&self, start: &str, end: &str, max_depth: usize) -> Vec<Path> {
356 let mut paths = Vec::new();
357 let mut current_path = Vec::new();
358 let mut visited = HashSet::new();
359
360 let mut state = PathSearchState {
361 path: &mut current_path,
362 visited: &mut visited,
363 all_paths: &mut paths,
364 weight: 0.0,
365 };
366
367 self.dfs_paths(start, end, &mut state, max_depth);
368
369 paths
370 }
371
372 fn dfs_paths(&self, current: &str, end: &str, state: &mut PathSearchState, max_depth: usize) {
374 if state.path.len() >= max_depth {
375 return;
376 }
377
378 state.path.push(current.to_string());
379 state.visited.insert(current.to_string());
380
381 if current == end {
382 state.all_paths.push(Path {
383 nodes: state.path.clone(),
384 weight: state.weight,
385 });
386 } else if let Some(neighbors) = self.adjacency.get(current) {
387 for (neighbor, edge_weight) in neighbors {
388 if !state.visited.contains(neighbor) {
389 let old_weight = state.weight;
390 state.weight += edge_weight;
391 self.dfs_paths(neighbor, end, state, max_depth);
392 state.weight = old_weight;
393 }
394 }
395 } else {
396 }
398
399 state.path.pop();
400 state.visited.remove(current);
401 }
402
403 pub fn top_degree_nodes(&self, top_k: usize) -> Vec<(String, f32)> {
411 let n = self.adjacency.len() as f32;
412 let mut scores: Vec<_> = self
413 .adjacency
414 .keys()
415 .map(|node| {
416 let degree = self.degree_centrality(node, n);
417 (node.clone(), degree)
418 })
419 .collect();
420
421 scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
422 scores.truncate(top_k);
423 scores
424 }
425
426 pub fn density(&self) -> f32 {
431 let n = self.adjacency.len() as f32;
432 let edge_count: usize = self.adjacency.values().map(|v| v.len()).sum();
433 let actual_edges = (edge_count / 2) as f32; if n > 1.0 {
436 (2.0 * actual_edges) / (n * (n - 1.0))
437 } else {
438 0.0
439 }
440 }
441
442 pub fn clustering_coefficient(&self) -> f32 {
447 let mut total = 0.0;
448 let mut count = 0;
449
450 for neighbors in self.adjacency.values() {
451 if neighbors.len() < 2 {
452 continue;
453 }
454
455 let neighbor_set: HashSet<_> = neighbors.iter().map(|(n, _)| n).collect();
456 let mut triangles = 0;
457
458 for (n1, _) in neighbors {
459 if let Some(n1_neighbors) = self.adjacency.get(n1) {
460 for (n2, _) in n1_neighbors {
461 if neighbor_set.contains(&n2) {
462 triangles += 1;
463 }
464 }
465 }
466 }
467
468 let k = neighbors.len() as f32;
469 let coefficient = triangles as f32 / (k * (k - 1.0));
470 total += coefficient;
471 count += 1;
472 }
473
474 if count > 0 {
475 total / count as f32
476 } else {
477 0.0
478 }
479 }
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485
486 fn create_test_graph() -> GraphAnalytics {
487 let edges = vec![
488 ("A".to_string(), "B".to_string(), 1.0),
489 ("A".to_string(), "C".to_string(), 1.0),
490 ("B".to_string(), "C".to_string(), 1.0),
491 ("B".to_string(), "D".to_string(), 1.0),
492 ("C".to_string(), "D".to_string(), 1.0),
493 ];
494 GraphAnalytics::new(edges)
495 }
496
497 #[test]
498 fn test_shortest_path() {
499 let graph = create_test_graph();
500 let path = graph.shortest_path("A", "D").unwrap();
501
502 assert_eq!(path.nodes.len(), 3); assert_eq!(path.weight, 2.0);
504 }
505
506 #[test]
507 fn test_centrality() {
508 let graph = create_test_graph();
509 let scores = graph.calculate_centrality();
510
511 assert!(scores.contains_key("A"));
512 assert!(scores.contains_key("B"));
513 assert!(scores.contains_key("C"));
514 assert!(scores.contains_key("D"));
515
516 let b_score = &scores["B"];
518 let a_score = &scores["A"];
519 assert!(b_score.betweenness >= a_score.betweenness);
520 }
521
522 #[test]
523 fn test_community_detection() {
524 let graph = create_test_graph();
525 let communities = graph.detect_communities();
526
527 assert_eq!(communities.len(), 1); assert_eq!(communities[0].nodes.len(), 4);
529 }
530
531 #[test]
532 fn test_density() {
533 let graph = create_test_graph();
534 let density = graph.density();
535
536 assert!(density > 0.0 && density <= 1.0);
537 }
538
539 #[test]
540 fn test_clustering() {
541 let graph = create_test_graph();
542 let coeff = graph.clustering_coefficient();
543
544 assert!(coeff >= 0.0 && coeff <= 1.0);
545 }
546}