Skip to main content

drft/analyses/
betweenness.rs

1use super::{Analysis, AnalysisContext};
2use std::collections::{HashMap, VecDeque};
3
4#[derive(Debug, Clone, serde::Serialize)]
5pub struct NodeBetweenness {
6    pub node: String,
7    pub score: f64,
8}
9
10#[derive(Debug, Clone, serde::Serialize)]
11pub struct BetweennessResult {
12    pub nodes: Vec<NodeBetweenness>,
13}
14
15pub struct Betweenness;
16
17impl Analysis for Betweenness {
18    type Output = BetweennessResult;
19
20    fn name(&self) -> &str {
21        "betweenness"
22    }
23
24    fn run(&self, ctx: &AnalysisContext) -> BetweennessResult {
25        let graph = ctx.graph;
26        let real_nodes: Vec<&str> = graph
27            .nodes
28            .keys()
29            .filter(|p| graph.is_file_node(p))
30            .map(|s| s.as_str())
31            .collect();
32
33        let n = real_nodes.len();
34        let mut centrality: HashMap<&str, f64> = HashMap::new();
35        for &node in &real_nodes {
36            centrality.insert(node, 0.0);
37        }
38
39        if n <= 2 {
40            let nodes = real_nodes
41                .iter()
42                .map(|&node| NodeBetweenness {
43                    node: node.to_string(),
44                    score: 0.0,
45                })
46                .collect();
47            let mut result = BetweennessResult { nodes };
48            result.nodes.sort_by(|a, b| a.node.cmp(&b.node));
49            return result;
50        }
51
52        // Brandes' algorithm (directed)
53        for &s in &real_nodes {
54            // BFS from s
55            let mut stack: Vec<&str> = Vec::new();
56            let mut predecessors: HashMap<&str, Vec<&str>> = HashMap::new();
57            let mut sigma: HashMap<&str, f64> = HashMap::new();
58            let mut dist: HashMap<&str, i64> = HashMap::new();
59
60            for &node in &real_nodes {
61                predecessors.insert(node, Vec::new());
62                sigma.insert(node, 0.0);
63                dist.insert(node, -1);
64            }
65
66            sigma.insert(s, 1.0);
67            dist.insert(s, 0);
68
69            let mut queue = VecDeque::new();
70            queue.push_back(s);
71
72            while let Some(v) = queue.pop_front() {
73                stack.push(v);
74                let v_dist = dist[v];
75
76                if let Some(edge_indices) = graph.forward.get(v) {
77                    for &idx in edge_indices {
78                        let w = graph.edges[idx].target.as_str();
79                        if !graph.is_file_node(w) {
80                            continue;
81                        }
82                        if dist[w] < 0 {
83                            dist.insert(w, v_dist + 1);
84                            queue.push_back(w);
85                        }
86                        if dist[w] == v_dist + 1 {
87                            *sigma.get_mut(w).unwrap() += sigma[v];
88                            predecessors.get_mut(w).unwrap().push(v);
89                        }
90                    }
91                }
92            }
93
94            // Back-propagation
95            let mut delta: HashMap<&str, f64> = HashMap::new();
96            for &node in &real_nodes {
97                delta.insert(node, 0.0);
98            }
99
100            while let Some(w) = stack.pop() {
101                for &v in &predecessors[w] {
102                    let d = (sigma[v] / sigma[w]) * (1.0 + delta[w]);
103                    *delta.get_mut(v).unwrap() += d;
104                }
105                if w != s {
106                    *centrality.get_mut(w).unwrap() += delta[w];
107                }
108            }
109        }
110
111        // Normalize by (n-1)*(n-2) for directed graphs
112        let norm = ((n - 1) * (n - 2)) as f64;
113        let mut nodes: Vec<NodeBetweenness> = centrality
114            .into_iter()
115            .map(|(node, score)| NodeBetweenness {
116                node: node.to_string(),
117                score: score / norm,
118            })
119            .collect();
120
121        // Sort by score descending, then node ascending
122        nodes.sort_by(|a, b| {
123            b.score
124                .partial_cmp(&a.score)
125                .unwrap()
126                .then_with(|| a.node.cmp(&b.node))
127        });
128
129        BetweennessResult { nodes }
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use crate::analyses::AnalysisContext;
137    use crate::config::Config;
138    use crate::graph::Graph;
139    use crate::graph::test_helpers::{make_edge, make_node};
140    use std::path::Path;
141
142    fn make_ctx<'a>(graph: &'a Graph, config: &'a Config) -> AnalysisContext<'a> {
143        AnalysisContext {
144            graph,
145            root: Path::new("."),
146            config,
147            lockfile: None,
148        }
149    }
150
151    #[test]
152    fn hub_node() {
153        let mut graph = Graph::new();
154        graph.add_node(make_node("a.md"));
155        graph.add_node(make_node("b.md"));
156        graph.add_node(make_node("c.md"));
157        graph.add_node(make_node("center.md"));
158        graph.add_node(make_node("d.md"));
159        graph.add_edge(make_edge("a.md", "center.md"));
160        graph.add_edge(make_edge("c.md", "center.md"));
161        graph.add_edge(make_edge("center.md", "b.md"));
162        graph.add_edge(make_edge("center.md", "d.md"));
163
164        let config = Config::defaults();
165        let result = Betweenness.run(&make_ctx(&graph, &config));
166        assert_eq!(result.nodes[0].node, "center.md");
167        assert!(result.nodes[0].score > 0.0);
168    }
169
170    #[test]
171    fn linear_chain() {
172        let mut graph = Graph::new();
173        graph.add_node(make_node("a.md"));
174        graph.add_node(make_node("b.md"));
175        graph.add_node(make_node("c.md"));
176        graph.add_node(make_node("d.md"));
177        graph.add_edge(make_edge("a.md", "b.md"));
178        graph.add_edge(make_edge("b.md", "c.md"));
179        graph.add_edge(make_edge("c.md", "d.md"));
180
181        let config = Config::defaults();
182        let result = Betweenness.run(&make_ctx(&graph, &config));
183        let b = result.nodes.iter().find(|n| n.node == "b.md").unwrap();
184        let c = result.nodes.iter().find(|n| n.node == "c.md").unwrap();
185        let a = result.nodes.iter().find(|n| n.node == "a.md").unwrap();
186        assert!(b.score > a.score);
187        assert!(c.score > a.score);
188    }
189
190    #[test]
191    fn single_node() {
192        let mut graph = Graph::new();
193        graph.add_node(make_node("a.md"));
194
195        let config = Config::defaults();
196        let result = Betweenness.run(&make_ctx(&graph, &config));
197        assert_eq!(result.nodes.len(), 1);
198        assert_eq!(result.nodes[0].score, 0.0);
199    }
200
201    #[test]
202    fn empty_graph() {
203        let graph = Graph::new();
204        let config = Config::defaults();
205        let result = Betweenness.run(&make_ctx(&graph, &config));
206        assert!(result.nodes.is_empty());
207    }
208}