Skip to main content

reddb_server/storage/engine/algorithms/
centrality.rs

1//! Centrality Algorithms
2//!
3//! Centrality measures for identifying important nodes:
4//! - Betweenness Centrality: Nodes on many shortest paths (chokepoints)
5//! - Closeness Centrality: Nodes close to all others (good attack starting points)
6//! - Degree Centrality: Nodes with many connections
7//! - Eigenvector Centrality: Nodes connected to other important nodes
8
9use std::collections::{HashMap, HashSet, VecDeque};
10
11use super::super::graph_store::GraphStore;
12
13// ============================================================================
14// Betweenness Centrality (Brandes' Algorithm)
15// ============================================================================
16
17/// Betweenness centrality computation using Brandes' algorithm
18///
19/// Betweenness centrality measures how often a node lies on shortest paths.
20/// High betweenness nodes are chokepoints - critical for network flow.
21pub struct BetweennessCentrality;
22
23/// Result of betweenness centrality computation
24#[derive(Debug, Clone)]
25pub struct BetweennessResult {
26    /// Node ID → betweenness centrality score
27    pub scores: HashMap<String, f64>,
28    /// Whether scores are normalized (divided by (n-1)(n-2))
29    pub normalized: bool,
30}
31
32impl BetweennessResult {
33    /// Get top N nodes by betweenness centrality
34    pub fn top(&self, n: usize) -> Vec<(String, f64)> {
35        let mut sorted: Vec<_> = self.scores.iter().map(|(k, v)| (k.clone(), *v)).collect();
36        sorted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
37        sorted.truncate(n);
38        sorted
39    }
40
41    /// Get score for a specific node
42    pub fn score(&self, node_id: &str) -> Option<f64> {
43        self.scores.get(node_id).copied()
44    }
45}
46
47impl BetweennessCentrality {
48    /// Compute betweenness centrality for all nodes
49    ///
50    /// Uses Brandes' algorithm: O(V*E) time, O(V) space
51    pub fn compute(graph: &GraphStore, normalize: bool) -> BetweennessResult {
52        let nodes: Vec<String> = graph.iter_nodes().map(|n| n.id.clone()).collect();
53        let n = nodes.len();
54
55        if n < 2 {
56            return BetweennessResult {
57                scores: nodes.into_iter().map(|id| (id, 0.0)).collect(),
58                normalized: normalize,
59            };
60        }
61
62        let mut centrality: HashMap<String, f64> =
63            nodes.iter().map(|id| (id.clone(), 0.0)).collect();
64
65        // Brandes' algorithm
66        for source in &nodes {
67            // Single-source shortest paths
68            let mut stack: Vec<String> = Vec::new();
69            let mut predecessors: HashMap<String, Vec<String>> = HashMap::new();
70            let mut sigma: HashMap<String, f64> =
71                nodes.iter().map(|id| (id.clone(), 0.0)).collect();
72            let mut dist: HashMap<String, i64> = nodes.iter().map(|id| (id.clone(), -1)).collect();
73
74            sigma.insert(source.clone(), 1.0);
75            dist.insert(source.clone(), 0);
76
77            let mut queue: VecDeque<String> = VecDeque::new();
78            queue.push_back(source.clone());
79
80            // BFS
81            while let Some(v) = queue.pop_front() {
82                stack.push(v.clone());
83                let v_dist = *dist.get(&v).unwrap_or(&0);
84
85                for (_, w, _) in graph.outgoing_edges(&v) {
86                    // w found for first time?
87                    if *dist.get(&w).unwrap_or(&-1) < 0 {
88                        queue.push_back(w.clone());
89                        dist.insert(w.clone(), v_dist + 1);
90                    }
91
92                    // Shortest path to w via v?
93                    if *dist.get(&w).unwrap_or(&0) == v_dist + 1 {
94                        let sigma_v = *sigma.get(&v).unwrap_or(&0.0);
95                        let sigma_w = sigma.entry(w.clone()).or_insert(0.0);
96                        *sigma_w += sigma_v;
97                        predecessors.entry(w.clone()).or_default().push(v.clone());
98                    }
99                }
100            }
101
102            // Accumulation
103            let mut delta: HashMap<String, f64> =
104                nodes.iter().map(|id| (id.clone(), 0.0)).collect();
105
106            while let Some(w) = stack.pop() {
107                if let Some(preds) = predecessors.get(&w) {
108                    let sigma_w = *sigma.get(&w).unwrap_or(&1.0);
109                    let delta_w = *delta.get(&w).unwrap_or(&0.0);
110
111                    for v in preds {
112                        let sigma_v = *sigma.get(v).unwrap_or(&1.0);
113                        let d = (sigma_v / sigma_w) * (1.0 + delta_w);
114                        *delta.entry(v.clone()).or_insert(0.0) += d;
115                    }
116                }
117
118                if w != *source {
119                    let c = centrality.entry(w.clone()).or_insert(0.0);
120                    *c += *delta.get(&w).unwrap_or(&0.0);
121                }
122            }
123        }
124
125        // Normalize if requested
126        if normalize && n > 2 {
127            let norm_factor = 1.0 / ((n - 1) * (n - 2)) as f64;
128            for score in centrality.values_mut() {
129                *score *= norm_factor;
130            }
131        }
132
133        BetweennessResult {
134            scores: centrality,
135            normalized: normalize,
136        }
137    }
138}
139
140// ============================================================================
141// Closeness Centrality
142// ============================================================================
143
144/// Closeness centrality measures how close a node is to all other nodes
145///
146/// High closeness = can reach all nodes quickly = good attack starting point
147/// Low closeness = isolated, harder to reach
148pub struct ClosenessCentrality;
149
150/// Result of closeness centrality computation
151#[derive(Debug, Clone)]
152pub struct ClosenessResult {
153    /// Node ID → closeness centrality (0 to 1, higher = more central)
154    pub scores: HashMap<String, f64>,
155}
156
157impl ClosenessResult {
158    /// Get top N nodes by closeness centrality
159    pub fn top(&self, n: usize) -> Vec<(String, f64)> {
160        let mut sorted: Vec<_> = self.scores.iter().map(|(k, v)| (k.clone(), *v)).collect();
161        sorted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
162        sorted.truncate(n);
163        sorted
164    }
165}
166
167impl ClosenessCentrality {
168    /// Compute closeness centrality for all nodes
169    ///
170    /// Closeness = (n-1) / sum(shortest_path_distances)
171    /// For disconnected graphs, uses harmonic closeness variant
172    pub fn compute(graph: &GraphStore) -> ClosenessResult {
173        let nodes: Vec<String> = graph.iter_nodes().map(|n| n.id.clone()).collect();
174        let n = nodes.len();
175
176        if n <= 1 {
177            return ClosenessResult {
178                scores: nodes.into_iter().map(|id| (id, 1.0)).collect(),
179            };
180        }
181
182        let mut scores: HashMap<String, f64> = HashMap::new();
183
184        for source in &nodes {
185            // BFS to find shortest paths from this node
186            let mut distances: HashMap<String, usize> = HashMap::new();
187            let mut queue: VecDeque<(String, usize)> = VecDeque::new();
188
189            queue.push_back((source.clone(), 0));
190            distances.insert(source.clone(), 0);
191
192            while let Some((current, dist)) = queue.pop_front() {
193                for (_, neighbor, _) in graph.outgoing_edges(&current) {
194                    // Single hash lookup: enqueue only on first visit.
195                    if let std::collections::hash_map::Entry::Vacant(slot) =
196                        distances.entry(neighbor)
197                    {
198                        let next_dist = dist + 1;
199                        queue.push_back((slot.key().clone(), next_dist));
200                        slot.insert(next_dist);
201                    }
202                }
203            }
204
205            // Calculate closeness (harmonic variant for disconnected graphs)
206            let sum_reciprocal: f64 = distances
207                .iter()
208                .filter(|(k, _)| *k != source)
209                .map(|(_, d)| 1.0 / (*d as f64))
210                .sum();
211
212            let closeness = sum_reciprocal / (n - 1) as f64;
213            scores.insert(source.clone(), closeness);
214        }
215
216        ClosenessResult { scores }
217    }
218}
219
220// ============================================================================
221// Degree Centrality
222// ============================================================================
223
224/// Degree centrality measures node importance by connection count
225///
226/// In security analysis:
227/// - High in-degree: Popular target (many paths lead here)
228/// - High out-degree: Key pivot point (can reach many targets)
229pub struct DegreeCentrality;
230
231/// Result of degree centrality computation
232#[derive(Debug, Clone)]
233pub struct DegreeCentralityResult {
234    /// Node ID → in-degree
235    pub in_degree: HashMap<String, usize>,
236    /// Node ID → out-degree
237    pub out_degree: HashMap<String, usize>,
238    /// Node ID → total degree (in + out)
239    pub total_degree: HashMap<String, usize>,
240}
241
242impl DegreeCentralityResult {
243    /// Get nodes sorted by total degree
244    pub fn top_by_total(&self, n: usize) -> Vec<(String, usize)> {
245        let mut sorted: Vec<_> = self
246            .total_degree
247            .iter()
248            .map(|(k, v)| (k.clone(), *v))
249            .collect();
250        sorted.sort_by_key(|b| std::cmp::Reverse(b.1));
251        sorted.truncate(n);
252        sorted
253    }
254
255    /// Get nodes sorted by in-degree
256    pub fn top_by_in_degree(&self, n: usize) -> Vec<(String, usize)> {
257        let mut sorted: Vec<_> = self
258            .in_degree
259            .iter()
260            .map(|(k, v)| (k.clone(), *v))
261            .collect();
262        sorted.sort_by_key(|b| std::cmp::Reverse(b.1));
263        sorted.truncate(n);
264        sorted
265    }
266
267    /// Get nodes sorted by out-degree
268    pub fn top_by_out_degree(&self, n: usize) -> Vec<(String, usize)> {
269        let mut sorted: Vec<_> = self
270            .out_degree
271            .iter()
272            .map(|(k, v)| (k.clone(), *v))
273            .collect();
274        sorted.sort_by_key(|b| std::cmp::Reverse(b.1));
275        sorted.truncate(n);
276        sorted
277    }
278}
279
280impl DegreeCentrality {
281    /// Compute degree centrality for all nodes
282    pub fn compute(graph: &GraphStore) -> DegreeCentralityResult {
283        let mut in_degree: HashMap<String, usize> = HashMap::new();
284        let mut out_degree: HashMap<String, usize> = HashMap::new();
285
286        // Initialize all nodes with 0 degree
287        for node in graph.iter_nodes() {
288            in_degree.insert(node.id.clone(), 0);
289            out_degree.insert(node.id.clone(), 0);
290        }
291
292        // Count degrees
293        for node in graph.iter_nodes() {
294            let out_count = graph.outgoing_edges(&node.id).len();
295            out_degree.insert(node.id.clone(), out_count);
296
297            // Count incoming edges by iterating targets
298            for (_, target, _) in graph.outgoing_edges(&node.id) {
299                *in_degree.entry(target).or_insert(0) += 1;
300            }
301        }
302
303        // Calculate total degree
304        let total_degree: HashMap<String, usize> = in_degree
305            .keys()
306            .map(|k| {
307                let total = in_degree.get(k).unwrap_or(&0) + out_degree.get(k).unwrap_or(&0);
308                (k.clone(), total)
309            })
310            .collect();
311
312        DegreeCentralityResult {
313            in_degree,
314            out_degree,
315            total_degree,
316        }
317    }
318}
319
320// ============================================================================
321// Eigenvector Centrality (Power Iteration)
322// ============================================================================
323
324/// Eigenvector centrality: importance based on neighbor importance
325///
326/// Like PageRank but without damping. A node is important if connected
327/// to other important nodes.
328pub struct EigenvectorCentrality {
329    /// Convergence threshold
330    pub epsilon: f64,
331    /// Maximum iterations
332    pub max_iterations: usize,
333}
334
335impl Default for EigenvectorCentrality {
336    fn default() -> Self {
337        Self {
338            epsilon: 1e-6,
339            max_iterations: 100,
340        }
341    }
342}
343
344/// Result of eigenvector centrality computation
345#[derive(Debug, Clone)]
346pub struct EigenvectorResult {
347    /// Node ID → eigenvector centrality score
348    pub scores: HashMap<String, f64>,
349    /// Number of iterations
350    pub iterations: usize,
351    /// Whether converged
352    pub converged: bool,
353}
354
355impl EigenvectorResult {
356    /// Get top N nodes by eigenvector centrality
357    pub fn top(&self, n: usize) -> Vec<(String, f64)> {
358        let mut sorted: Vec<_> = self.scores.iter().map(|(k, v)| (k.clone(), *v)).collect();
359        sorted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
360        sorted.truncate(n);
361        sorted
362    }
363}
364
365impl EigenvectorCentrality {
366    pub fn new() -> Self {
367        Self::default()
368    }
369
370    /// Compute eigenvector centrality using power iteration
371    pub fn compute(&self, graph: &GraphStore) -> EigenvectorResult {
372        let nodes: Vec<String> = graph.iter_nodes().map(|n| n.id.clone()).collect();
373        let n = nodes.len();
374
375        if n == 0 {
376            return EigenvectorResult {
377                scores: HashMap::new(),
378                iterations: 0,
379                converged: true,
380            };
381        }
382
383        // Build adjacency (treat as undirected for eigenvector centrality)
384        let mut neighbors: HashMap<String, Vec<String>> = HashMap::new();
385        for node in &nodes {
386            let mut node_neighbors: HashSet<String> = HashSet::new();
387            for (_, target, _) in graph.outgoing_edges(node) {
388                node_neighbors.insert(target);
389            }
390            for (_, source, _) in graph.incoming_edges(node) {
391                node_neighbors.insert(source);
392            }
393            neighbors.insert(node.clone(), node_neighbors.into_iter().collect());
394        }
395
396        // Initialize scores uniformly
397        let init_score = 1.0 / (n as f64).sqrt();
398        let mut scores: HashMap<String, f64> =
399            nodes.iter().map(|id| (id.clone(), init_score)).collect();
400
401        let mut converged = false;
402        let mut iterations = 0;
403
404        for iter in 0..self.max_iterations {
405            iterations = iter + 1;
406            let mut new_scores: HashMap<String, f64> = HashMap::new();
407
408            // Calculate new scores (sum of neighbor scores)
409            for node in &nodes {
410                let sum: f64 = neighbors
411                    .get(node)
412                    .map(|nbrs| {
413                        nbrs.iter()
414                            .map(|n| scores.get(n).copied().unwrap_or(0.0))
415                            .sum()
416                    })
417                    .unwrap_or(0.0);
418                new_scores.insert(node.clone(), sum);
419            }
420
421            // Normalize (L2 norm)
422            let norm: f64 = new_scores.values().map(|v| v * v).sum::<f64>().sqrt();
423            if norm > 0.0 {
424                for score in new_scores.values_mut() {
425                    *score /= norm;
426                }
427            }
428
429            // Check convergence
430            let diff: f64 = nodes
431                .iter()
432                .map(|id| {
433                    let old = scores.get(id).copied().unwrap_or(0.0);
434                    let new = new_scores.get(id).copied().unwrap_or(0.0);
435                    (old - new).abs()
436                })
437                .sum();
438
439            scores = new_scores;
440
441            if diff < self.epsilon {
442                converged = true;
443                break;
444            }
445        }
446
447        EigenvectorResult {
448            scores,
449            iterations,
450            converged,
451        }
452    }
453}